From f125c0e6527c2265f5e655a83a4d6b79342c98a8 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 27 Feb 2018 12:36:35 +0530 Subject: [PATCH] Add docs folder --- dist/frappe-datatable.cjs.css | 1 + dist/frappe-datatable.cjs.js | 4214 +++++++++++++++++---------------- dist/frappe-datatable.css | 1 + dist/frappe-datatable.js | 4214 +++++++++++++++++---------------- docs/frappe-datatable.css | 1 + docs/frappe-datatable.js | 4214 +++++++++++++++++---------------- docs/index.css | 14 +- docs/index.html | 7 +- docs/index.js | 616 ++++- 9 files changed, 7134 insertions(+), 6148 deletions(-) diff --git a/dist/frappe-datatable.cjs.css b/dist/frappe-datatable.cjs.css index 90ab6d1..30dd637 100644 --- a/dist/frappe-datatable.cjs.css +++ b/dist/frappe-datatable.cjs.css @@ -1,3 +1,4 @@ +/* This file is processed by postcss */ /* variables */ .data-table { diff --git a/dist/frappe-datatable.cjs.js b/dist/frappe-datatable.cjs.js index 2bfc30e..0d76138 100644 --- a/dist/frappe-datatable.cjs.js +++ b/dist/frappe-datatable.cjs.js @@ -6,175 +6,185 @@ var Sortable = _interopDefault(require('sortablejs')); var Clusterize = _interopDefault(require('clusterize.js')); function $(expr, con) { - return typeof expr === 'string' ? - (con || document).querySelector(expr) : - expr || null; + 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; + return typeof expr === 'string' ? + Array.from((con || document).querySelectorAll(expr)) : + expr || null; }; $.create = (tag, o) => { - let element = document.createElement(tag); + let element = document.createElement(tag); - for (let i in o) { - let val = o[i]; + 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]; - }); - } + if (i === 'inside') { + $(val).appendChild(element); } else - if (i in element) { + 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 { + } else { element.setAttribute(i, val); - } - } + } + } - return element; + return element; }; $.on = (element, event, selector, callback) => { - if (!callback) { - callback = selector; - $.bind(element, event, callback); - } else { - $.delegate(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); + element.removeEventListener(event, handler); }; $.bind = (element, event, callback) => { - event.split(/\s+/).forEach(function (event) { - element.addEventListener(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); - } - }); + 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]; + if (element) { + for (let event in o) { + let callback = o[event]; - event.split(/\s+/).forEach(function (event) { - element.removeEventListener(event, callback); - }); + event.split(/\s+/).forEach(function (event) { + element.removeEventListener(event, callback); + }); + } } - } }; $.fire = (target, type, properties) => { - let evt = document.createEvent('HTMLEvents'); + let evt = document.createEvent('HTMLEvents'); - evt.initEvent(type, true, true); + evt.initEvent(type, true, true); - for (let j in properties) { - evt[j] = properties[j]; - } + for (let j in properties) { + evt[j] = properties[j]; + } - return target.dispatchEvent(evt); + return target.dispatchEvent(evt); }; $.data = (element, attrs) => { // eslint-disable-line - if (!attrs) { - return element.dataset; - } + if (!attrs) { + return element.dataset; + } - for (const attr in attrs) { - element.dataset[attr] = attrs[attr]; - } + 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]; + 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] = ''; + 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]; + let val = getComputedStyle(element)[prop]; - if (['width', 'height'].includes(prop)) { - val = parseFloat(val); - } + if (['width', 'height'].includes(prop)) { + val = parseFloat(val); + } - return val; + return val; }; $.closest = (selector, element) => { - if (!element) return null; + if (!element) return null; - if (element.matches(selector)) { - return element; - } + if (element.matches(selector)) { + return element; + } - return $.closest(selector, element.parentNode); + 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(); + 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; + return top >= pTop && left >= pLeft && bottom <= pBottom && right <= pRight; }; $.scrollTop = function scrollTop(element, pixels) { - requestAnimationFrame(() => { - element.scrollTop = pixels; - }); + requestAnimationFrame(() => { + element.scrollTop = pixels; + }); }; /** @@ -695,26 +705,26 @@ function throttle(func, wait, options) { var throttle_1 = throttle; function camelCaseToDash(str) { - return str.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); + return str.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); } function makeDataAttributeString(props) { - const keys = Object.keys(props); + const keys = Object.keys(props); - return keys - .map((key) => { - const _key = camelCaseToDash(key); - const val = props[key]; + return keys + .map((key) => { + const _key = camelCaseToDash(key); + const val = props[key]; - if (val === undefined) return ''; - return `data-${_key}="${val}" `; - }) - .join('') - .trim(); + if (val === undefined) return ''; + return `data-${_key}="${val}" `; + }) + .join('') + .trim(); } function getDefault(a, b) { - return a !== undefined ? a : b; + return a !== undefined ? a : b; } @@ -728,62 +738,62 @@ function getDefault(a, b) { function copyTextToClipboard(text) { - // https://stackoverflow.com/a/30810322/5353542 - var textArea = document.createElement('textarea'); + // 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. - // + // + // *** 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; + // 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'; + // 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; + // 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'; + // 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'; + // Avoid flash of white box if rendered for any reason. + textArea.style.background = 'transparent'; - textArea.value = text; + textArea.value = text; - document.body.appendChild(textArea); + document.body.appendChild(textArea); - textArea.select(); + textArea.select(); - try { - document.execCommand('copy'); - } catch (err) { - console.log('Oops, unable to copy'); - } + try { + document.execCommand('copy'); + } catch (err) { + console.log('Oops, unable to copy'); + } - document.body.removeChild(textArea); + document.body.removeChild(textArea); } function isNumeric(val) { - return !isNaN(val); + return !isNaN(val); } let throttle$1 = throttle_1; @@ -791,936 +801,986 @@ 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); - }); - }; + 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); + 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; + 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); } - 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 - }; - - 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(''); + init(data) { + if (!data) { + data = this.options.data; } - } 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(d[col.id]); - } + 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); } - } - return this.prepareRow(row, index); - }); - } + if (this.options.addSerialNoColumn && !this.hasColumnById('_rowIndex')) { + let cell = { + id: '_rowIndex', + content: '', + align: 'center', + editable: false, + resizable: false, + focusable: false, + dropdown: false + }; - validateColumns() { - const columns = this.options.columns; - if (!Array.isArray(columns)) { - throw new DataError('`columns` must be an array'); + this.columns.push(cell); + } } - columns.forEach((column, i) => { - if (typeof column !== 'string' && typeof column !== 'object') { - throw new DataError(`column "${i}" must be a string or an object`); - } - }); - } + 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 + ''; + } + }; - validateData(data) { - if (Array.isArray(data) && - (data.length === 0 || Array.isArray(data[0]) || typeof data[0] === 'object')) { - return true; + 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; + }); } - throw new DataError('`data` must be an array of arrays or objects'); - } - appendRows(rows) { - this.validateData(rows); + prepareCell(content, i) { + const cell = { + content: '', + align: 'left', + sortOrder: 'none', + colIndex: i, + column: this.columns[i] + }; - 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; + if (content !== null && typeof content === 'object') { + // passed as column/header + Object.assign(cell, content); } else { - col.sortOrder = 'none'; + cell.content = content; } - }); - 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; - } + return cell; } - 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; + prepareNumericColumns() { + const row0 = this.getRow(0); + if (!row0) return; + this.columns = this.columns.map((column, i) => { - 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; - }); + const cellValue = row0[i].content; + if (!column.align && cellValue && isNumeric(cellValue)) { + column.align = 'right'; + } - 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; + return column; }); - }); - } - } - - 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; + prepareRows() { + this.validateData(this.data); - return _row; - } + this.rows = this.data.map((d, i) => { + const index = this._getNextRowCount(); - 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); + let row = []; - // mutate object directly - for (let key in options) { - const newVal = options[key]; - if (newVal !== undefined) { - cell[key] = newVal; - } + 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 an object + 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(d[col.id]); + } + } + } + + return this.prepareRow(row, { + rowIndex: index + }); + }); } - return cell; - } + prepareRow(row, props) { + const baseRowCell = { + rowIndex: props.rowIndex + }; - 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; - } + row = row + .map((cell, i) => this.prepareCell(cell, i)) + .map(cell => Object.assign({}, baseRowCell, cell)); - 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()); + // monkey patched in array object + row.meta = props; + return row; } - return columns; - } + validateColumns() { + const columns = this.options.columns; + if (!Array.isArray(columns)) { + throw new DataError('`columns` must be an array'); + } - getStandardColumnCount() { - if (this.options.addCheckboxColumn && this.options.addSerialNoColumn) { - return 2; + columns.forEach((column, i) => { + if (typeof column !== 'string' && typeof column !== 'object') { + throw new DataError(`column "${i}" must be a string or an object`); + } + }); } - if (this.options.addCheckboxColumn || this.options.addSerialNoColumn) { - return 1; + 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'); } - return 0; - } + appendRows(rows) { + this.validateData(rows); - getColumnCount(skipStandardColumns) { - let val = this.columns.length; - - if (skipStandardColumns) { - val = val - this.getStandardColumnCount(); + this.rows = this.rows.concat(this.prepareRows(rows)); } - return val; - } + sortRows(colIndex, sortOrder = 'none') { + colIndex = +colIndex; - getColumn(colIndex) { - colIndex = +colIndex; - return this.columns.find(col => col.colIndex === colIndex); - } + // reset sortOrder and update for colIndex + this.getColumns() + .map(col => { + if (col.colIndex === colIndex) { + col.sortOrder = sortOrder; + } else { + col.sortOrder = 'none'; + } + }); - getRow(rowIndex) { - rowIndex = +rowIndex; - return this.rows.find(row => row[0].rowIndex === rowIndex); - } + this._sortRows(colIndex, sortOrder); + } - getCell(colIndex, rowIndex) { - rowIndex = +rowIndex; - colIndex = +colIndex; - return this.rows.find(row => row[0].rowIndex === rowIndex)[colIndex]; - } + _sortRows(colIndex, sortOrder) { - get() { - return { - columns: this.columns, - rows: this.rows - }; - } + 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; + } + } - hasColumn(name) { - return Boolean(this.columns.find(col => col.content === name)); - } + 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; - hasColumnById(id) { - return Boolean(this.columns.find(col => col.id === id)); - } + 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; + }); - getColumnIndex(name) { - return this.columns.findIndex(col => col.content === name); - } + 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; + }); + }); + } + } - getColumnIndexById(id) { - return this.columns.findIndex(col => col.id === id); - } + reverseArray(array) { + let left = null; + let right = null; + let length = array.length; - getCheckboxHTML() { - return ''; - } + 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; + constructor(instance) { + this.instance = instance; - linkProperties(this, this.instance, [ - 'options', - 'fireEvent', - 'header', - 'datamanager', - 'style', - 'wrapper', - 'rowmanager', - 'bodyScrollable' - ]); + linkProperties(this, this.instance, [ + 'options', + 'fireEvent', + 'header', + 'datamanager', + 'style', + 'wrapper', + 'rowmanager', + 'bodyScrollable' + ]); - this.bindEvents(); - getDropdownHTML = getDropdownHTML.bind(this, this.options.dropdownButton); - } + this.bindEvents(); + getDropdownHTML = getDropdownHTML.bind(this, this.options.dropdownButton); + } - renderHeader() { - this.header.innerHTML = ''; - this.refreshHeader(); - } + renderHeader() { + this.header.innerHTML = ''; + this.refreshHeader(); + } - refreshHeader() { - const columns = this.datamanager.getColumns(); + refreshHeader() { + const columns = this.datamanager.getColumns(); - if (!$('.data-table-col', this.header)) { - // insert html + 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 }); - } + let html = this.rowmanager.getRowHTML(columns, { + isHeader: 1 + }); + if (this.options.enableInlineFilters) { + html += this.rowmanager.getRowHTML(columns, { + isFilter: 1 + }); + } - $('thead', this.header).innerHTML = html; + $('thead', this.header).innerHTML = html; - this.$filterRow = $('.data-table-row[data-is-filter]', this.header); + 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; - } + 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 - }); + $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]; + // refresh sort indicator + const sortIndicator = $('.sort-indicator', $col); + if (sortIndicator) { + sortIndicator.innerHTML = this.options.sortIndicator[column.sortOrder]; + } + }); } - }); + // reset columnMap + this.$columnMap = []; } - // 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; + bindEvents() { + this.bindDropdown(); + this.bindResizeColumn(); + this.bindMoveColumn(); + this.bindFilter(); } - } - bindResizeColumn() { - let isDragging = false; - let $resizingCell, startWidth, startX; + bindDropdown() { + let $activeDropdown; + $.on(this.header, 'click', '.data-table-dropdown-toggle', (e, $button) => { + const $dropdown = $.closest('.data-table-dropdown', $button); - $.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' + if (!$dropdown.classList.contains('is-active')) { + deactivateDropdown(); + $dropdown.classList.add('is-active'); + $activeDropdown = $dropdown; + } else { + deactivateDropdown(); + } }); - }); - 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(document.body, 'click', (e) => { + if (e.target.matches('.data-table-dropdown-toggle')) return; + deactivateDropdown(); }); - }; - $.on(this.header, 'keydown', '.data-table-filter', debounce$2(handler, 300)); - } - sortRows(colIndex, sortOrder) { - return this.datamanager.sortRows(colIndex, sortOrder); - } + const dropdownItems = this.options.headerDropdown; - getColumn(colIndex) { - return this.datamanager.getColumn(colIndex); - } + $.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; - getColumns() { - return this.datamanager.getColumns(); - } + callback && callback.call(this.instance, this.getColumn(colIndex)); + }); - 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; + function deactivateDropdown(e) { + $activeDropdown && $activeDropdown.classList.remove('is-active'); + $activeDropdown = null; + } } - $column.style.width = width + 'px'; - } + bindResizeColumn() { + let isDragging = false; + let $resizingCell, startWidth, startX; - getColumnMinWidth(colIndex) { - colIndex = +colIndex; - return this.getColumn(colIndex).minWidth || 24; - } + $.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); - getFirstColumnIndex() { - if (this.options.addCheckboxColumn && this.options.addSerialNoColumn) { - return 2; + 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); + }); } - if (this.options.addCheckboxColumn || this.options.addSerialNoColumn) { - return 1; + 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); } - return 0; - } + bindSortColumn() { - getHeaderCell$(colIndex) { - return $(`.data-table-col[data-col-index="${colIndex}"]`, this.header); - } + $.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); - getLastColumnIndex() { - return this.datamanager.getColumnCount() - 1; - } + if (col && col.sortable === false) { + return; + } - getSerialColumnIndex() { - const columns = this.datamanager.getColumns(); + // reset sort indicator + $('.sort-indicator', this.header).textContent = ''; + $.each('.data-table-col', this.header).map($cell => { + $.data($cell, { + sortOrder: 'none' + }); + }); - return columns.findIndex(column => column.content.includes('Sr. No')); - } + 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; + // add dropdown buttons + const dropdownItems = this.options.headerDropdown; - return `
${dropdownButton}
+ return `
${dropdownButton}
${dropdownItems.map((d, i) => `
${d.label}
`).join('')}
@@ -1728,603 +1788,649 @@ var getDropdownHTML = function getDropdownHTML(dropdownButton = 'v') { }; class CellManager { - constructor(instance) { - this.instance = instance; - this.wrapper = this.instance.wrapper; - this.options = this.instance.options; - this.style = this.instance.style; - this.bodyScrollable = this.instance.bodyScrollable; - this.columnmanager = this.instance.columnmanager; - this.rowmanager = this.instance.rowmanager; - this.datamanager = this.instance.datamanager; - this.keyboard = this.instance.keyboard; + constructor(instance) { + this.instance = instance; + this.wrapper = this.instance.wrapper; + this.options = this.instance.options; + this.style = this.instance.style; + this.bodyScrollable = this.instance.bodyScrollable; + this.columnmanager = this.instance.columnmanager; + this.rowmanager = this.instance.rowmanager; + this.datamanager = this.instance.datamanager; + this.keyboard = this.instance.keyboard; - this.bindEvents(); - } + this.bindEvents(); + } - bindEvents() { - this.bindFocusCell(); - this.bindEditCell(); - this.bindKeyboardSelection(); - this.bindCopyCellContents(); - this.bindMouseEvents(); - } + bindEvents() { + this.bindFocusCell(); + this.bindEditCell(); + this.bindKeyboardSelection(); + this.bindCopyCellContents(); + this.bindMouseEvents(); + } - bindFocusCell() { - this.bindKeyboardNav(); - } + bindFocusCell() { + this.bindKeyboardNav(); + } - bindEditCell() { - this.$editingCell = null; + bindEditCell() { + this.$editingCell = null; - $.on(this.bodyScrollable, 'dblclick', '.data-table-col', (e, cell) => { - this.activateEditing(cell); - }); + $.on(this.bodyScrollable, 'dblclick', '.data-table-col', (e, cell) => { + this.activateEditing(cell); + }); + + this.keyboard.on('enter', (e) => { + if (this.$focusedCell && !this.$editingCell) { + // enter keypress on focused cell + this.activateEditing(this.$focusedCell); + } else if (this.$editingCell) { + // enter keypress on editing cell + this.submitEditing(); + this.deactivateEditing(); + } + }); + } + + bindKeyboardNav() { + const focusCell = (direction) => { + if (!this.$focusedCell || this.$editingCell) { + return false; + } + + let $cell = this.$focusedCell; + + if (direction === 'left' || direction === 'shift+tab') { + $cell = this.getLeftCell$($cell); + } else if (direction === 'right' || direction === 'tab') { + $cell = this.getRightCell$($cell); + } else if (direction === 'up') { + $cell = this.getAboveCell$($cell); + } else if (direction === 'down') { + $cell = this.getBelowCell$($cell); + } + + this.focusCell($cell); + return true; + }; + + const focusLastCell = (direction) => { + if (!this.$focusedCell || this.$editingCell) { + return false; + } + + let $cell = this.$focusedCell; + const { + rowIndex, + colIndex + } = $.data($cell); + + if (direction === 'left') { + $cell = this.getLeftMostCell$(rowIndex); + } else if (direction === 'right') { + $cell = this.getRightMostCell$(rowIndex); + } else if (direction === 'up') { + $cell = this.getTopMostCell$(colIndex); + } else if (direction === 'down') { + $cell = this.getBottomMostCell$(colIndex); + } + + this.focusCell($cell); + return true; + }; + + ['left', 'right', 'up', 'down', 'tab', 'shift+tab'].map( + direction => this.keyboard.on(direction, () => focusCell(direction)) + ); + + ['left', 'right', 'up', 'down'].map( + direction => this.keyboard.on('ctrl+' + direction, () => focusLastCell(direction)) + ); + + this.keyboard.on('esc', () => { + this.deactivateEditing(); + }); + + if (this.options.enableInlineFilters) { + this.keyboard.on('ctrl+f', (e) => { + const $cell = $.closest('.data-table-col', e.target); + let { + colIndex + } = $.data($cell); + + this.activateFilter(colIndex); + return true; + }); + } + } + + bindKeyboardSelection() { + const getNextSelectionCursor = (direction) => { + let $selectionCursor = this.getSelectionCursor(); + + if (direction === 'left') { + $selectionCursor = this.getLeftCell$($selectionCursor); + } else if (direction === 'right') { + $selectionCursor = this.getRightCell$($selectionCursor); + } else if (direction === 'up') { + $selectionCursor = this.getAboveCell$($selectionCursor); + } else if (direction === 'down') { + $selectionCursor = this.getBelowCell$($selectionCursor); + } + + return $selectionCursor; + }; + + ['left', 'right', 'up', 'down'].map( + direction => this.keyboard.on('shift+' + direction, + () => this.selectArea(getNextSelectionCursor(direction))) + ); + } + + bindCopyCellContents() { + this.keyboard.on('ctrl+c', () => { + this.copyCellContents(this.$focusedCell, this.$selectionCursor); + }); + } + + bindMouseEvents() { + let mouseDown = null; + + $.on(this.bodyScrollable, 'mousedown', '.data-table-col', (e) => { + mouseDown = true; + this.focusCell($(e.delegatedTarget)); + }); + + $.on(this.bodyScrollable, 'mouseup', () => { + mouseDown = false; + }); + + const selectArea = (e) => { + if (!mouseDown) return; + this.selectArea($(e.delegatedTarget)); + }; + + $.on(this.bodyScrollable, 'mousemove', '.data-table-col', throttle$1(selectArea, 50)); + } + + focusCell($cell, { + skipClearSelection = 0 + } = {}) { + if (!$cell) return; + + // don't focus if already editing cell + if ($cell === this.$editingCell) return; + + const { + colIndex, + isHeader + } = $.data($cell); + if (isHeader) { + return; + } + + const column = this.columnmanager.getColumn(colIndex); + if (column.focusable === false) { + return; + } + + this.scrollToCell($cell); - this.keyboard.on('enter', (e) => { - if (this.$focusedCell && !this.$editingCell) { - // enter keypress on focused cell - this.activateEditing(this.$focusedCell); - } else if (this.$editingCell) { - // enter keypress on editing cell - this.submitEditing(); this.deactivateEditing(); - } - }); - } + if (!skipClearSelection) { + this.clearSelection(); + } - bindKeyboardNav() { - const focusCell = (direction) => { - if (!this.$focusedCell || this.$editingCell) { - return false; - } + if (this.$focusedCell) { + this.$focusedCell.classList.remove('selected'); + } - let $cell = this.$focusedCell; + this.$focusedCell = $cell; + $cell.classList.add('selected'); - if (direction === 'left' || direction === 'shift+tab') { - $cell = this.getLeftCell$($cell); - } else if (direction === 'right' || direction === 'tab') { - $cell = this.getRightCell$($cell); - } else if (direction === 'up') { - $cell = this.getAboveCell$($cell); - } else if (direction === 'down') { - $cell = this.getBelowCell$($cell); - } - - this.focusCell($cell); - return true; - }; - - const focusLastCell = (direction) => { - if (!this.$focusedCell || this.$editingCell) { - return false; - } - - let $cell = this.$focusedCell; - const { rowIndex, colIndex } = $.data($cell); - - if (direction === 'left') { - $cell = this.getLeftMostCell$(rowIndex); - } else if (direction === 'right') { - $cell = this.getRightMostCell$(rowIndex); - } else if (direction === 'up') { - $cell = this.getTopMostCell$(colIndex); - } else if (direction === 'down') { - $cell = this.getBottomMostCell$(colIndex); - } - - this.focusCell($cell); - return true; - }; - - ['left', 'right', 'up', 'down', 'tab', 'shift+tab'].map( - direction => this.keyboard.on(direction, () => focusCell(direction)) - ); - - ['left', 'right', 'up', 'down'].map( - direction => this.keyboard.on('ctrl+' + direction, () => focusLastCell(direction)) - ); - - this.keyboard.on('esc', () => { - this.deactivateEditing(); - }); - - if (this.options.enableInlineFilters) { - this.keyboard.on('ctrl+f', (e) => { - const $cell = $.closest('.data-table-col', e.target); - let { colIndex } = $.data($cell); - - this.activateFilter(colIndex); - return true; - }); - } - } - - bindKeyboardSelection() { - const getNextSelectionCursor = (direction) => { - let $selectionCursor = this.getSelectionCursor(); - - if (direction === 'left') { - $selectionCursor = this.getLeftCell$($selectionCursor); - } else if (direction === 'right') { - $selectionCursor = this.getRightCell$($selectionCursor); - } else if (direction === 'up') { - $selectionCursor = this.getAboveCell$($selectionCursor); - } else if (direction === 'down') { - $selectionCursor = this.getBelowCell$($selectionCursor); - } - - return $selectionCursor; - }; - - ['left', 'right', 'up', 'down'].map( - direction => this.keyboard.on('shift+' + direction, - () => this.selectArea(getNextSelectionCursor(direction))) - ); - } - - bindCopyCellContents() { - this.keyboard.on('ctrl+c', () => { - this.copyCellContents(this.$focusedCell, this.$selectionCursor); - }); - } - - bindMouseEvents() { - let mouseDown = null; - - $.on(this.bodyScrollable, 'mousedown', '.data-table-col', (e) => { - mouseDown = true; - this.focusCell($(e.delegatedTarget)); - }); - - $.on(this.bodyScrollable, 'mouseup', () => { - mouseDown = false; - }); - - const selectArea = (e) => { - if (!mouseDown) return; - this.selectArea($(e.delegatedTarget)); - }; - - $.on(this.bodyScrollable, 'mousemove', '.data-table-col', throttle$1(selectArea, 50)); - } - - focusCell($cell, { skipClearSelection = 0 } = {}) { - if (!$cell) return; - - // don't focus if already editing cell - if ($cell === this.$editingCell) return; - - const { colIndex, isHeader } = $.data($cell); - if (isHeader) { - return; - } - - const column = this.columnmanager.getColumn(colIndex); - if (column.focusable === false) { - return; - } - - this.scrollToCell($cell); - - this.deactivateEditing(); - if (!skipClearSelection) { - this.clearSelection(); - } - - if (this.$focusedCell) { - this.$focusedCell.classList.remove('selected'); - } - - this.$focusedCell = $cell; - $cell.classList.add('selected'); - - // so that keyboard nav works - $cell.focus(); - - this.highlightRowColumnHeader($cell); - } - - highlightRowColumnHeader($cell) { - const { colIndex, rowIndex } = $.data($cell); - const _colIndex = this.datamanager.getColumnIndexById('_rowIndex'); - const colHeaderSelector = `.data-table-header .data-table-col[data-col-index="${colIndex}"]`; - const rowHeaderSelector = `.data-table-col[data-row-index="${rowIndex}"][data-col-index="${_colIndex}"]`; - - if (this.lastHeaders) { - $.removeStyle(this.lastHeaders, 'backgroundColor'); - } - - const colHeader = $(colHeaderSelector, this.wrapper); - const rowHeader = $(rowHeaderSelector, this.wrapper); - - $.style([colHeader, rowHeader], { - backgroundColor: '#f5f7fa' // light-bg - }); - - this.lastHeaders = [colHeader, rowHeader]; - } - - selectAreaOnClusterChanged() { - if (!(this.$focusedCell && this.$selectionCursor)) return; - const { colIndex, rowIndex } = $.data(this.$selectionCursor); - const $cell = this.getCell$(colIndex, rowIndex); - - if (!$cell || $cell === this.$selectionCursor) return; - - // selectArea needs $focusedCell - const fCell = $.data(this.$focusedCell); - this.$focusedCell = this.getCell$(fCell.colIndex, fCell.rowIndex); - - this.selectArea($cell); - } - - focusCellOnClusterChanged() { - if (!this.$focusedCell) return; - - const { colIndex, rowIndex } = $.data(this.$focusedCell); - const $cell = this.getCell$(colIndex, rowIndex); - - if (!$cell) return; - // this function is called after selectAreaOnClusterChanged, - // focusCell calls clearSelection which resets the area selection - // so a flag to skip it - this.focusCell($cell, { skipClearSelection: 1 }); - } - - selectArea($selectionCursor) { - if (!this.$focusedCell) return; - - if (this._selectArea(this.$focusedCell, $selectionCursor)) { - // valid selection - this.$selectionCursor = $selectionCursor; - } - }; - - _selectArea($cell1, $cell2) { - if ($cell1 === $cell2) return false; - - const cells = this.getCellsInRange($cell1, $cell2); - if (!cells) return false; - - this.clearSelection(); - cells.map(index => this.getCell$(...index)).map($cell => $cell.classList.add('highlight')); - return true; - } - - getCellsInRange($cell1, $cell2) { - let colIndex1, rowIndex1, colIndex2, rowIndex2; - - if (typeof $cell1 === 'number') { - [colIndex1, rowIndex1, colIndex2, rowIndex2] = arguments; - } else - if (typeof $cell1 === 'object') { - - if (!($cell1 && $cell2)) { - return false; - } - - const cell1 = $.data($cell1); - const cell2 = $.data($cell2); - - colIndex1 = cell1.colIndex; - rowIndex1 = cell1.rowIndex; - colIndex2 = cell2.colIndex; - rowIndex2 = cell2.rowIndex; - } - - if (rowIndex1 > rowIndex2) { - [rowIndex1, rowIndex2] = [rowIndex2, rowIndex1]; - } - - if (colIndex1 > colIndex2) { - [colIndex1, colIndex2] = [colIndex2, colIndex1]; - } - - if (this.isStandardCell(colIndex1) || this.isStandardCell(colIndex2)) { - return false; - } - - let cells = []; - let colIndex = colIndex1; - let rowIndex = rowIndex1; - let rowIndices = []; - - while (rowIndex <= rowIndex2) { - rowIndices.push(rowIndex); - rowIndex++; - } - - rowIndices.map(rowIndex => { - while (colIndex <= colIndex2) { - cells.push([colIndex, rowIndex]); - colIndex++; - } - colIndex = colIndex1; - }); - - return cells; - } - - clearSelection() { - $.each('.data-table-col.highlight', this.bodyScrollable) - .map(cell => cell.classList.remove('highlight')); - - this.$selectionCursor = null; - } - - getSelectionCursor() { - return this.$selectionCursor || this.$focusedCell; - } - - activateEditing($cell) { - this.focusCell($cell); - const { rowIndex, colIndex } = $.data($cell); - - const col = this.columnmanager.getColumn(colIndex); - if (col && (col.editable === false || col.focusable === false)) { - return; - } - - const cell = this.getCell(colIndex, rowIndex); - if (cell && cell.editable === false) { - return; - } - - if (this.$editingCell) { - const { _rowIndex, _colIndex } = $.data(this.$editingCell); - - if (rowIndex === _rowIndex && colIndex === _colIndex) { - // editing the same cell - return; - } - } - - this.$editingCell = $cell; - $cell.classList.add('editing'); - - const $editCell = $('.edit-cell', $cell); - $editCell.innerHTML = ''; - - const editor = this.getEditor(colIndex, rowIndex, cell.content, $editCell); - - if (editor) { - this.currentCellEditor = editor; - // initialize editing input with cell value - editor.initValue(cell.content, rowIndex, col); - } - } - - deactivateEditing() { - // keep focus on the cell so that keyboard navigation works - if (this.$focusedCell) this.$focusedCell.focus(); - - if (!this.$editingCell) return; - this.$editingCell.classList.remove('editing'); - this.$editingCell = null; - } - - getEditor(colIndex, rowIndex, value, parent) { - // debugger; - const obj = this.options.getEditor(colIndex, rowIndex, value, parent); - if (obj && obj.setValue) return obj; - - // editing fallback - const $input = $.create('input', { - class: 'input-style', - type: 'text', - inside: parent - }); - - return { - initValue(value) { - $input.focus(); - $input.value = value; - }, - getValue() { - return $input.value; - }, - setValue(value) { - $input.value = value; - } - }; - } - - submitEditing() { - if (!this.$editingCell) return; - const $cell = this.$editingCell; - const { rowIndex, colIndex } = $.data($cell); - const col = this.datamanager.getColumn(colIndex); - - if ($cell) { - const editor = this.currentCellEditor; - - if (editor) { - const value = editor.getValue(); - const done = editor.setValue(value, rowIndex, col); - const oldValue = this.getCell(colIndex, rowIndex).content; - - // update cell immediately - this.updateCell(colIndex, rowIndex, value); + // so that keyboard nav works $cell.focus(); - if (done && done.then) { - // revert to oldValue if promise fails - done.catch((e) => { - console.log(e); - this.updateCell(colIndex, rowIndex, oldValue); - }); + this.highlightRowColumnHeader($cell); + } + + highlightRowColumnHeader($cell) { + const { + colIndex, + rowIndex + } = $.data($cell); + const _colIndex = this.datamanager.getColumnIndexById('_rowIndex'); + const colHeaderSelector = `.data-table-header .data-table-col[data-col-index="${colIndex}"]`; + const rowHeaderSelector = `.data-table-col[data-row-index="${rowIndex}"][data-col-index="${_colIndex}"]`; + + if (this.lastHeaders) { + $.removeStyle(this.lastHeaders, 'backgroundColor'); } - } + + const colHeader = $(colHeaderSelector, this.wrapper); + const rowHeader = $(rowHeaderSelector, this.wrapper); + + $.style([colHeader, rowHeader], { + backgroundColor: '#f5f7fa' // light-bg + }); + + this.lastHeaders = [colHeader, rowHeader]; } - this.currentCellEditor = null; - } + selectAreaOnClusterChanged() { + if (!(this.$focusedCell && this.$selectionCursor)) return; + const { + colIndex, + rowIndex + } = $.data(this.$selectionCursor); + const $cell = this.getCell$(colIndex, rowIndex); - copyCellContents($cell1, $cell2) { - if (!$cell2 && $cell1) { - // copy only focusedCell - const { colIndex, rowIndex } = $.data($cell1); - const cell = this.getCell(colIndex, rowIndex); - copyTextToClipboard(cell.content); - return; + if (!$cell || $cell === this.$selectionCursor) return; + + // selectArea needs $focusedCell + const fCell = $.data(this.$focusedCell); + this.$focusedCell = this.getCell$(fCell.colIndex, fCell.rowIndex); + + this.selectArea($cell); } - const cells = this.getCellsInRange($cell1, $cell2); - if (!cells) return; + focusCellOnClusterChanged() { + if (!this.$focusedCell) return; - const values = cells - // get cell objects - .map(index => this.getCell(...index)) - // convert to array of rows - .reduce((acc, curr) => { - const rowIndex = curr.rowIndex; + const { + colIndex, + rowIndex + } = $.data(this.$focusedCell); + const $cell = this.getCell$(colIndex, rowIndex); - acc[rowIndex] = acc[rowIndex] || []; - acc[rowIndex].push(curr.content); - - return acc; - }, []) - // join values by tab - .map(row => row.join('\t')) - // join rows by newline - .join('\n'); - - copyTextToClipboard(values); - } - - activateFilter(colIndex) { - this.columnmanager.toggleFilter(); - this.columnmanager.focusFilter(colIndex); - - if (!this.columnmanager.isFilterShown) { - // put focus back on cell - this.$focusedCell.focus(); + if (!$cell) return; + // this function is called after selectAreaOnClusterChanged, + // focusCell calls clearSelection which resets the area selection + // so a flag to skip it + this.focusCell($cell, { + skipClearSelection: 1 + }); } - } - updateCell(colIndex, rowIndex, value) { - const cell = this.datamanager.updateCell(colIndex, rowIndex, { - content: value - }); - this.refreshCell(cell); - } + selectArea($selectionCursor) { + if (!this.$focusedCell) return; - refreshCell(cell) { - const $cell = $(this.cellSelector(cell.colIndex, cell.rowIndex), this.bodyScrollable); - $cell.innerHTML = this.getCellContent(cell); - } + if (this._selectArea(this.$focusedCell, $selectionCursor)) { + // valid selection + this.$selectionCursor = $selectionCursor; + } + }; - isStandardCell(colIndex) { - // Standard cells are in Sr. No and Checkbox column - return colIndex < this.columnmanager.getFirstColumnIndex(); - } + _selectArea($cell1, $cell2) { + if ($cell1 === $cell2) return false; - getCell$(colIndex, rowIndex) { - return $(this.cellSelector(colIndex, rowIndex), this.bodyScrollable); - } + const cells = this.getCellsInRange($cell1, $cell2); + if (!cells) return false; - getAboveCell$($cell) { - const { colIndex } = $.data($cell); - const $aboveRow = $cell.parentElement.previousElementSibling; + this.clearSelection(); + cells.map(index => this.getCell$(...index)).map($cell => $cell.classList.add('highlight')); + return true; + } - return $(`[data-col-index="${colIndex}"]`, $aboveRow); - } + getCellsInRange($cell1, $cell2) { + let colIndex1, rowIndex1, colIndex2, rowIndex2; - getBelowCell$($cell) { - const { colIndex } = $.data($cell); - const $belowRow = $cell.parentElement.nextElementSibling; + if (typeof $cell1 === 'number') { + [colIndex1, rowIndex1, colIndex2, rowIndex2] = arguments; + } else + if (typeof $cell1 === 'object') { - return $(`[data-col-index="${colIndex}"]`, $belowRow); - } + if (!($cell1 && $cell2)) { + return false; + } - getLeftCell$($cell) { - return $cell.previousElementSibling; - } + const cell1 = $.data($cell1); + const cell2 = $.data($cell2); - getRightCell$($cell) { - return $cell.nextElementSibling; - } + colIndex1 = cell1.colIndex; + rowIndex1 = cell1.rowIndex; + colIndex2 = cell2.colIndex; + rowIndex2 = cell2.rowIndex; + } - getLeftMostCell$(rowIndex) { - return this.getCell$(this.columnmanager.getFirstColumnIndex(), rowIndex); - } + if (rowIndex1 > rowIndex2) { + [rowIndex1, rowIndex2] = [rowIndex2, rowIndex1]; + } - getRightMostCell$(rowIndex) { - return this.getCell$(this.columnmanager.getLastColumnIndex(), rowIndex); - } + if (colIndex1 > colIndex2) { + [colIndex1, colIndex2] = [colIndex2, colIndex1]; + } - getTopMostCell$(colIndex) { - return this.getCell$(colIndex, this.rowmanager.getFirstRowIndex()); - } + if (this.isStandardCell(colIndex1) || this.isStandardCell(colIndex2)) { + return false; + } - getBottomMostCell$(colIndex) { - return this.getCell$(colIndex, this.rowmanager.getLastRowIndex()); - } + let cells = []; + let colIndex = colIndex1; + let rowIndex = rowIndex1; + let rowIndices = []; - getCell(colIndex, rowIndex) { - return this.instance.datamanager.getCell(colIndex, rowIndex); - } + while (rowIndex <= rowIndex2) { + rowIndices.push(rowIndex); + rowIndex++; + } - getCellAttr($cell) { - return this.instance.getCellAttr($cell); - } + rowIndices.map(rowIndex => { + while (colIndex <= colIndex2) { + cells.push([colIndex, rowIndex]); + colIndex++; + } + colIndex = colIndex1; + }); - getRowHeight() { - return $.style($('.data-table-row', this.bodyScrollable), 'height'); - } + return cells; + } - scrollToCell($cell) { - if ($.inViewport($cell, this.bodyScrollable)) return false; + clearSelection() { + $.each('.data-table-col.highlight', this.bodyScrollable) + .map(cell => cell.classList.remove('highlight')); - const { rowIndex } = $.data($cell); - this.rowmanager.scrollToRow(rowIndex); - return false; - } + this.$selectionCursor = null; + } - getRowCountPerPage() { - return Math.ceil(this.instance.getViewportHeight() / this.getRowHeight()); - } + getSelectionCursor() { + return this.$selectionCursor || this.$focusedCell; + } - getCellHTML(cell) { - const { rowIndex, colIndex, isHeader, isFilter } = cell; - const dataAttr = makeDataAttributeString({ - rowIndex, - colIndex, - isHeader, - isFilter - }); + activateEditing($cell) { + this.focusCell($cell); + const { + rowIndex, + colIndex + } = $.data($cell); - return ` + const col = this.columnmanager.getColumn(colIndex); + if (col && (col.editable === false || col.focusable === false)) { + return; + } + + const cell = this.getCell(colIndex, rowIndex); + if (cell && cell.editable === false) { + return; + } + + if (this.$editingCell) { + const { + _rowIndex, + _colIndex + } = $.data(this.$editingCell); + + if (rowIndex === _rowIndex && colIndex === _colIndex) { + // editing the same cell + return; + } + } + + this.$editingCell = $cell; + $cell.classList.add('editing'); + + const $editCell = $('.edit-cell', $cell); + $editCell.innerHTML = ''; + + const editor = this.getEditor(colIndex, rowIndex, cell.content, $editCell); + + if (editor) { + this.currentCellEditor = editor; + // initialize editing input with cell value + editor.initValue(cell.content, rowIndex, col); + } + } + + deactivateEditing() { + // keep focus on the cell so that keyboard navigation works + if (this.$focusedCell) this.$focusedCell.focus(); + + if (!this.$editingCell) return; + this.$editingCell.classList.remove('editing'); + this.$editingCell = null; + } + + getEditor(colIndex, rowIndex, value, parent) { + // debugger; + const obj = this.options.getEditor(colIndex, rowIndex, value, parent); + if (obj && obj.setValue) return obj; + + // editing fallback + const $input = $.create('input', { + class: 'input-style', + type: 'text', + inside: parent + }); + + return { + initValue(value) { + $input.focus(); + $input.value = value; + }, + getValue() { + return $input.value; + }, + setValue(value) { + $input.value = value; + } + }; + } + + submitEditing() { + if (!this.$editingCell) return; + const $cell = this.$editingCell; + const { + rowIndex, + colIndex + } = $.data($cell); + const col = this.datamanager.getColumn(colIndex); + + if ($cell) { + const editor = this.currentCellEditor; + + if (editor) { + const value = editor.getValue(); + const done = editor.setValue(value, rowIndex, col); + const oldValue = this.getCell(colIndex, rowIndex).content; + + // update cell immediately + this.updateCell(colIndex, rowIndex, value); + $cell.focus(); + + if (done && done.then) { + // revert to oldValue if promise fails + done.catch((e) => { + console.log(e); + this.updateCell(colIndex, rowIndex, oldValue); + }); + } + } + } + + this.currentCellEditor = null; + } + + copyCellContents($cell1, $cell2) { + if (!$cell2 && $cell1) { + // copy only focusedCell + const { + colIndex, + rowIndex + } = $.data($cell1); + const cell = this.getCell(colIndex, rowIndex); + copyTextToClipboard(cell.content); + return; + } + const cells = this.getCellsInRange($cell1, $cell2); + + if (!cells) return; + + const values = cells + // get cell objects + .map(index => this.getCell(...index)) + // convert to array of rows + .reduce((acc, curr) => { + const rowIndex = curr.rowIndex; + + acc[rowIndex] = acc[rowIndex] || []; + acc[rowIndex].push(curr.content); + + return acc; + }, []) + // join values by tab + .map(row => row.join('\t')) + // join rows by newline + .join('\n'); + + copyTextToClipboard(values); + } + + activateFilter(colIndex) { + this.columnmanager.toggleFilter(); + this.columnmanager.focusFilter(colIndex); + + if (!this.columnmanager.isFilterShown) { + // put focus back on cell + this.$focusedCell.focus(); + } + } + + updateCell(colIndex, rowIndex, value) { + const cell = this.datamanager.updateCell(colIndex, rowIndex, { + content: value + }); + this.refreshCell(cell); + } + + refreshCell(cell) { + const $cell = $(this.cellSelector(cell.colIndex, cell.rowIndex), this.bodyScrollable); + $cell.innerHTML = this.getCellContent(cell); + } + + isStandardCell(colIndex) { + // Standard cells are in Sr. No and Checkbox column + return colIndex < this.columnmanager.getFirstColumnIndex(); + } + + getCell$(colIndex, rowIndex) { + return $(this.cellSelector(colIndex, rowIndex), this.bodyScrollable); + } + + getAboveCell$($cell) { + const { + colIndex + } = $.data($cell); + const $aboveRow = $cell.parentElement.previousElementSibling; + + return $(`[data-col-index="${colIndex}"]`, $aboveRow); + } + + getBelowCell$($cell) { + const { + colIndex + } = $.data($cell); + const $belowRow = $cell.parentElement.nextElementSibling; + + return $(`[data-col-index="${colIndex}"]`, $belowRow); + } + + getLeftCell$($cell) { + return $cell.previousElementSibling; + } + + getRightCell$($cell) { + return $cell.nextElementSibling; + } + + getLeftMostCell$(rowIndex) { + return this.getCell$(this.columnmanager.getFirstColumnIndex(), rowIndex); + } + + getRightMostCell$(rowIndex) { + return this.getCell$(this.columnmanager.getLastColumnIndex(), rowIndex); + } + + getTopMostCell$(colIndex) { + return this.getCell$(colIndex, this.rowmanager.getFirstRowIndex()); + } + + getBottomMostCell$(colIndex) { + return this.getCell$(colIndex, this.rowmanager.getLastRowIndex()); + } + + getCell(colIndex, rowIndex) { + return this.instance.datamanager.getCell(colIndex, rowIndex); + } + + getCellAttr($cell) { + return this.instance.getCellAttr($cell); + } + + getRowHeight() { + return $.style($('.data-table-row', this.bodyScrollable), 'height'); + } + + scrollToCell($cell) { + if ($.inViewport($cell, this.bodyScrollable)) return false; + + const { + rowIndex + } = $.data($cell); + this.rowmanager.scrollToRow(rowIndex); + return false; + } + + getRowCountPerPage() { + return Math.ceil(this.instance.getViewportHeight() / this.getRowHeight()); + } + + getCellHTML(cell) { + const { + rowIndex, + colIndex, + isHeader, + isFilter + } = cell; + const dataAttr = makeDataAttributeString({ + rowIndex, + colIndex, + isHeader, + isFilter + }); + + return ` ${this.getCellContent(cell)} `; - } - - getCellContent(cell) { - const { isHeader } = cell; - - const editable = !isHeader && cell.editable !== false; - const editCellHTML = editable ? this.getEditCellHTML() : ''; - - const sortable = isHeader && cell.sortable !== false; - const sortIndicator = sortable ? '' : ''; - - const resizable = isHeader && cell.resizable !== false; - const resizeColumn = resizable ? '' : ''; - - const hasDropdown = isHeader && cell.dropdown !== false; - const dropdown = hasDropdown ? `
${getDropdownHTML()}
` : ''; - - let contentHTML; - if (cell.isHeader || cell.isFilter || !cell.column.format) { - contentHTML = cell.content; - } else { - contentHTML = cell.column.format(cell.content, cell); } - return ` + getCellContent(cell) { + const { + isHeader + } = cell; + + const editable = !isHeader && cell.editable !== false; + const editCellHTML = editable ? this.getEditCellHTML() : ''; + + const sortable = isHeader && cell.sortable !== false; + const sortIndicator = sortable ? '' : ''; + + const resizable = isHeader && cell.resizable !== false; + const resizeColumn = resizable ? '' : ''; + + const hasDropdown = isHeader && cell.dropdown !== false; + const dropdown = hasDropdown ? `
${getDropdownHTML()}
` : ''; + + let contentHTML; + if (cell.isHeader || cell.isFilter || !cell.column.format) { + contentHTML = cell.content; + } else { + contentHTML = cell.column.format(cell.content, cell); + } + + return `
${(contentHTML)} ${sortIndicator} @@ -2333,321 +2439,332 @@ class CellManager {
${editCellHTML} `; - } + } - getEditCellHTML() { - return ` + getEditCellHTML() { + return `
`; - } + } - cellSelector(colIndex, rowIndex) { - return `.data-table-col[data-col-index="${colIndex}"][data-row-index="${rowIndex}"]`; - } + cellSelector(colIndex, rowIndex) { + return `.data-table-col[data-col-index="${colIndex}"][data-row-index="${rowIndex}"]`; + } } class RowManager { - constructor(instance) { - this.instance = instance; - this.options = this.instance.options; - this.wrapper = this.instance.wrapper; - this.bodyScrollable = this.instance.bodyScrollable; + constructor(instance) { + this.instance = instance; + this.options = this.instance.options; + this.wrapper = this.instance.wrapper; + this.bodyScrollable = this.instance.bodyScrollable; - this.bindEvents(); - this.refreshRows = promisify(this.refreshRows, this); - } - - get datamanager() { - return this.instance.datamanager; - } - - get cellmanager() { - return this.instance.cellmanager; - } - - bindEvents() { - this.bindCheckbox(); - } - - bindCheckbox() { - if (!this.options.addCheckboxColumn) return; - - // map of checked rows - this.checkMap = []; - - $.on(this.wrapper, 'click', '.data-table-col[data-col-index="0"] [type="checkbox"]', (e, $checkbox) => { - const $cell = $checkbox.closest('.data-table-col'); - const { rowIndex, isHeader } = $.data($cell); - const checked = $checkbox.checked; - - if (isHeader) { - this.checkAll(checked); - } else { - this.checkRow(rowIndex, checked); - } - }); - } - - refreshRows() { - this.instance.renderBody(); - this.instance.setDimensions(); - } - - refreshRow(row, rowIndex) { - const _row = this.datamanager.updateRow(row, rowIndex); - - _row.forEach(cell => { - this.cellmanager.refreshCell(cell); - }); - } - - getCheckedRows() { - if (!this.checkMap) { - return []; + this.bindEvents(); + this.refreshRows = promisify(this.refreshRows, this); } - return this.checkMap - .map((c, rowIndex) => { - if (c) { - return rowIndex; + get datamanager() { + return this.instance.datamanager; + } + + get cellmanager() { + return this.instance.cellmanager; + } + + bindEvents() { + this.bindCheckbox(); + } + + bindCheckbox() { + if (!this.options.addCheckboxColumn) return; + + // map of checked rows + this.checkMap = []; + + $.on(this.wrapper, 'click', '.data-table-col[data-col-index="0"] [type="checkbox"]', (e, $checkbox) => { + const $cell = $checkbox.closest('.data-table-col'); + const { + rowIndex, + isHeader + } = $.data($cell); + const checked = $checkbox.checked; + + if (isHeader) { + this.checkAll(checked); + } else { + this.checkRow(rowIndex, checked); + } + }); + } + + refreshRows() { + this.instance.renderBody(); + this.instance.setDimensions(); + } + + refreshRow(row, rowIndex) { + const _row = this.datamanager.updateRow(row, rowIndex); + + _row.forEach(cell => { + this.cellmanager.refreshCell(cell); + }); + } + + getCheckedRows() { + if (!this.checkMap) { + return []; } - return null; - }) - .filter(c => { - return c !== null || c !== undefined; - }); - } - highlightCheckedRows() { - this.getCheckedRows() - .map(rowIndex => this.checkRow(rowIndex, true)); - } - - checkRow(rowIndex, toggle) { - const value = toggle ? 1 : 0; - - // update internal map - this.checkMap[rowIndex] = value; - // set checkbox value explicitly - $.each(`.data-table-col[data-row-index="${rowIndex}"][data-col-index="0"] [type="checkbox"]`, this.bodyScrollable) - .map(input => { - input.checked = toggle; - }); - // highlight row - this.highlightRow(rowIndex, toggle); - } - - checkAll(toggle) { - const value = toggle ? 1 : 0; - - // update internal map - if (toggle) { - this.checkMap = Array.from(Array(this.getTotalRows())).map(c => value); - } else { - this.checkMap = []; - } - // set checkbox value - $.each('.data-table-col[data-col-index="0"] [type="checkbox"]', this.bodyScrollable) - .map(input => { - input.checked = toggle; - }); - // highlight all - this.highlightAll(toggle); - } - - highlightRow(rowIndex, toggle = true) { - const $row = this.getRow$(rowIndex); - if (!$row) return; - - if (!toggle && this.bodyScrollable.classList.contains('row-highlight-all')) { - $row.classList.add('row-unhighlight'); - return; + return this.checkMap + .map((c, rowIndex) => { + if (c) { + return rowIndex; + } + return null; + }) + .filter(c => { + return c !== null || c !== undefined; + }); } - if (toggle && $row.classList.contains('row-unhighlight')) { - $row.classList.remove('row-unhighlight'); + highlightCheckedRows() { + this.getCheckedRows() + .map(rowIndex => this.checkRow(rowIndex, true)); } - this._highlightedRows = this._highlightedRows || {}; - - if (toggle) { - $row.classList.add('row-highlight'); - this._highlightedRows[rowIndex] = $row; - } else { - $row.classList.remove('row-highlight'); - delete this._highlightedRows[rowIndex]; - } - } - - highlightAll(toggle = true) { - if (toggle) { - this.bodyScrollable.classList.add('row-highlight-all'); - } else { - this.bodyScrollable.classList.remove('row-highlight-all'); - for (const rowIndex in this._highlightedRows) { - const $row = this._highlightedRows[rowIndex]; - $row.classList.remove('row-highlight'); - } - this._highlightedRows = {}; - } - } - - getRow$(rowIndex) { - return $(`.data-table-row[data-row-index="${rowIndex}"]`, this.bodyScrollable); - } - - getTotalRows() { - return this.datamanager.getRowCount(); - } - - getFirstRowIndex() { - return 0; - } - - getLastRowIndex() { - return this.datamanager.getRowCount() - 1; - } - - scrollToRow(rowIndex) { - rowIndex = +rowIndex; - this._lastScrollTo = this._lastScrollTo || 0; - const $row = this.getRow$(rowIndex); - if ($.inViewport($row, this.bodyScrollable)) return; - - const { height } = $row.getBoundingClientRect(); - const { top, bottom } = this.bodyScrollable.getBoundingClientRect(); - const rowsInView = Math.floor((bottom - top) / height); - - let offset = 0; - if (rowIndex > this._lastScrollTo) { - offset = height * ((rowIndex + 1) - rowsInView); - } else { - offset = height * ((rowIndex + 1) - 1); + checkRow(rowIndex, toggle) { + const value = toggle ? 1 : 0; + const selector = rowIndex => + `.data-table-col[data-row-index="${rowIndex}"][data-col-index="0"] [type="checkbox"]`; + // update internal map + this.checkMap[rowIndex] = value; + // set checkbox value explicitly + $.each(selector(rowIndex), this.bodyScrollable) + .map(input => { + input.checked = toggle; + }); + // highlight row + this.highlightRow(rowIndex, toggle); } - this._lastScrollTo = rowIndex; - $.scrollTop(this.bodyScrollable, offset); - } + checkAll(toggle) { + const value = toggle ? 1 : 0; - getRowHTML(row, props) { - const dataAttr = makeDataAttributeString(props); - - if (props.isFilter) { - row = row.map(cell => (Object.assign(cell, { - content: this.getFilterInput({ colIndex: cell.colIndex }), - isFilter: 1, - isHeader: undefined, - editable: false - }))); + // update internal map + if (toggle) { + this.checkMap = Array.from(Array(this.getTotalRows())).map(c => value); + } else { + this.checkMap = []; + } + // set checkbox value + $.each('.data-table-col[data-col-index="0"] [type="checkbox"]', this.bodyScrollable) + .map(input => { + input.checked = toggle; + }); + // highlight all + this.highlightAll(toggle); } - return ` - - ${row.map(cell => this.cellmanager.getCellHTML(cell)).join('')} - - `; - } + highlightRow(rowIndex, toggle = true) { + const $row = this.getRow$(rowIndex); + if (!$row) return; - getFilterInput(props) { - const dataAttr = makeDataAttributeString(props); - return ``; - } + if (!toggle && this.bodyScrollable.classList.contains('row-highlight-all')) { + $row.classList.add('row-unhighlight'); + return; + } + + if (toggle && $row.classList.contains('row-unhighlight')) { + $row.classList.remove('row-unhighlight'); + } + + this._highlightedRows = this._highlightedRows || {}; + + if (toggle) { + $row.classList.add('row-highlight'); + this._highlightedRows[rowIndex] = $row; + } else { + $row.classList.remove('row-highlight'); + delete this._highlightedRows[rowIndex]; + } + } + + highlightAll(toggle = true) { + if (toggle) { + this.bodyScrollable.classList.add('row-highlight-all'); + } else { + this.bodyScrollable.classList.remove('row-highlight-all'); + for (const rowIndex in this._highlightedRows) { + const $row = this._highlightedRows[rowIndex]; + $row.classList.remove('row-highlight'); + } + this._highlightedRows = {}; + } + } + + getRow$(rowIndex) { + return $(`.data-table-row[data-row-index="${rowIndex}"]`, this.bodyScrollable); + } + + getTotalRows() { + return this.datamanager.getRowCount(); + } + + getFirstRowIndex() { + return 0; + } + + getLastRowIndex() { + return this.datamanager.getRowCount() - 1; + } + + scrollToRow(rowIndex) { + rowIndex = +rowIndex; + this._lastScrollTo = this._lastScrollTo || 0; + const $row = this.getRow$(rowIndex); + if ($.inViewport($row, this.bodyScrollable)) return; + + const { + height + } = $row.getBoundingClientRect(); + const { + top, + bottom + } = this.bodyScrollable.getBoundingClientRect(); + const rowsInView = Math.floor((bottom - top) / height); + + let offset = 0; + if (rowIndex > this._lastScrollTo) { + offset = height * ((rowIndex + 1) - rowsInView); + } else { + offset = height * ((rowIndex + 1) - 1); + } + + this._lastScrollTo = rowIndex; + $.scrollTop(this.bodyScrollable, offset); + } + + getRowHTML(row, props) { + const dataAttr = makeDataAttributeString(props); + + if (props.isFilter) { + row = row.map(cell => (Object.assign(cell, { + content: this.getFilterInput({ + colIndex: cell.colIndex + }), + isFilter: 1, + isHeader: undefined, + editable: false + }))); + } + + return ` + + ${row.map(cell => this.cellmanager.getCellHTML(cell)).join('')} + + `; + } + + getFilterInput(props) { + const dataAttr = makeDataAttributeString(props); + return ``; + } } class BodyRenderer { - constructor(instance) { - this.instance = instance; - this.options = instance.options; - this.datamanager = instance.datamanager; - this.rowmanager = instance.rowmanager; - this.cellmanager = instance.cellmanager; - this.bodyScrollable = instance.bodyScrollable; - this.log = instance.log; - this.appendRemainingData = promisify(this.appendRemainingData, this); - } - - render() { - if (this.options.enableClusterize) { - this.renderBodyWithClusterize(); - } else { - this.renderBodyHTML(); - } - } - - renderBodyHTML() { - const rows = this.datamanager.getRows(); - - this.bodyScrollable.innerHTML = ` - - ${getBodyHTML(rows)} -
- `; - this.instance.setDimensions(); - this.restoreState(); - } - - renderBodyWithClusterize() { - // first page - const rows = this.datamanager.getRows(0, 20); - const initialData = this.getDataForClusterize(rows); - - if (!this.clusterize) { - // empty body - this.bodyScrollable.innerHTML = ` - - ${getBodyHTML([])} -
- `; - - // first 20 rows will appended - // rest of them in nextTick - this.clusterize = new Clusterize({ - rows: initialData, - scrollElem: this.bodyScrollable, - contentElem: $('tbody', this.bodyScrollable), - callbacks: { - clusterChanged: () => { - this.restoreState(); - } - }, - /* eslint-disable */ - no_data_text: this.options.noDataMessage, - no_data_class: 'empty-state' - /* eslint-enable */ - }); - - // setDimensions requires atleast 1 row to exist in dom - this.instance.setDimensions(); - } else { - this.clusterize.update(initialData); + constructor(instance) { + this.instance = instance; + this.options = instance.options; + this.datamanager = instance.datamanager; + this.rowmanager = instance.rowmanager; + this.cellmanager = instance.cellmanager; + this.bodyScrollable = instance.bodyScrollable; + this.log = instance.log; + this.appendRemainingData = promisify(this.appendRemainingData, this); } - this.appendRemainingData(); - } + render() { + if (this.options.enableClusterize) { + this.renderBodyWithClusterize(); + } else { + this.renderBodyHTML(); + } + } - restoreState() { - this.rowmanager.highlightCheckedRows(); - this.cellmanager.selectAreaOnClusterChanged(); - this.cellmanager.focusCellOnClusterChanged(); - } + renderBodyHTML() { + const rows = this.datamanager.getRows(); - appendRemainingData() { - const rows = this.datamanager.getRows(20); - const data = this.getDataForClusterize(rows); - this.clusterize.append(data); - } + this.bodyScrollable.innerHTML = ` + + ${this.getBodyHTML(rows)} +
+ `; + this.instance.setDimensions(); + this.restoreState(); + } - getDataForClusterize(rows) { - return rows.map((row) => this.rowmanager.getRowHTML(row, { rowIndex: row[0].rowIndex })); - } -} + renderBodyWithClusterize() { + // first page + const rows = this.datamanager.getRows(0, 20); + const initialData = this.getDataForClusterize(rows); -function getBodyHTML(rows) { - return ` - - ${rows.map(row => this.rowmanager.getRowHTML(row, { rowIndex: row[0].rowIndex })).join('')} - - `; + if (!this.clusterize) { + // empty body + this.bodyScrollable.innerHTML = ` + + ${this.getBodyHTML([])} +
+ `; + + // first 20 rows will appended + // rest of them in nextTick + this.clusterize = new Clusterize({ + rows: initialData, + scrollElem: this.bodyScrollable, + contentElem: $('tbody', this.bodyScrollable), + callbacks: { + clusterChanged: () => { + this.restoreState(); + } + }, + /* eslint-disable */ + no_data_text: this.options.noDataMessage, + no_data_class: 'empty-state' + /* eslint-enable */ + }); + + // setDimensions requires atleast 1 row to exist in dom + this.instance.setDimensions(); + } else { + this.clusterize.update(initialData); + } + + this.appendRemainingData(); + } + + restoreState() { + this.rowmanager.highlightCheckedRows(); + this.cellmanager.selectAreaOnClusterChanged(); + this.cellmanager.focusCellOnClusterChanged(); + } + + appendRemainingData() { + const rows = this.datamanager.getRows(20); + const data = this.getDataForClusterize(rows); + this.clusterize.append(data); + } + + getDataForClusterize(rows) { + return rows.map((row) => this.rowmanager.getRowHTML(row, row.meta)); + } + + getBodyHTML(rows) { + return ` + + ${rows.map(row => this.rowmanager.getRowHTML(row, row.meta)).join('')} + + `; + } } class Style { @@ -2684,7 +2801,7 @@ class Style { this.styleEl.remove(); } - setStyle(rule, styleMap, index = -1) { + setStyle(selector, styleMap, index = -1) { const styles = Object.keys(styleMap) .map(prop => { if (!prop.includes('-')) { @@ -2693,7 +2810,12 @@ class Style { return `${prop}:${styleMap[prop]};`; }) .join(''); - let ruleString = `.${this.scopeClass} ${rule} { ${styles} }`; + let prefixedSelector = selector + .split(',') + .map(r => `.${this.scopeClass} ${r}`) + .join(','); + + let ruleString = `${prefixedSelector} { ${styles} }`; let _index = this.styleSheet.cssRules.length; if (index !== -1) { @@ -2881,157 +3003,155 @@ class Style { } const KEYCODES = { - 13: 'enter', - 91: 'meta', - 16: 'shift', - 17: 'ctrl', - 18: 'alt', - 37: 'left', - 38: 'up', - 39: 'right', - 40: 'down', - 9: 'tab', - 27: 'esc', - 67: 'c', - 70: 'f' + 13: 'enter', + 91: 'meta', + 16: 'shift', + 17: 'ctrl', + 18: 'alt', + 37: 'left', + 38: 'up', + 39: 'right', + 40: 'down', + 9: 'tab', + 27: 'esc', + 67: 'c', + 70: 'f' }; class Keyboard { - constructor(element) { - this.listeners = {}; - $.on(element, 'keydown', this.handler.bind(this)); - } - - handler(e) { - let key = KEYCODES[e.keyCode]; - - if (e.shiftKey && key !== 'shift') { - key = 'shift+' + key; + constructor(element) { + this.listeners = {}; + $.on(element, 'keydown', this.handler.bind(this)); } - if ((e.ctrlKey && key !== 'ctrl') || (e.metaKey && key !== 'meta')) { - key = 'ctrl+' + key; - } + handler(e) { + let key = KEYCODES[e.keyCode]; - const listeners = this.listeners[key]; - - if (listeners && listeners.length > 0) { - for (let listener of listeners) { - const preventBubbling = listener(e); - if (preventBubbling === undefined || preventBubbling === true) { - e.preventDefault(); + if (e.shiftKey && key !== 'shift') { + key = 'shift+' + key; + } + + if ((e.ctrlKey && key !== 'ctrl') || (e.metaKey && key !== 'meta')) { + key = 'ctrl+' + key; + } + + const listeners = this.listeners[key]; + + if (listeners && listeners.length > 0) { + for (let listener of listeners) { + const preventBubbling = listener(e); + if (preventBubbling === undefined || preventBubbling === true) { + e.preventDefault(); + } + } } - } } - } - on(key, listener) { - const keys = key.split(',').map(k => k.trim()); + on(key, listener) { + const keys = key.split(',').map(k => k.trim()); - keys.map(key => { - this.listeners[key] = this.listeners[key] || []; - this.listeners[key].push(listener); - }); - } + keys.map(key => { + this.listeners[key] = this.listeners[key] || []; + this.listeners[key].push(listener); + }); + } } var DEFAULT_OPTIONS = { - columns: [], - data: [], - dropdownButton: '▼', - headerDropdown: [ - { - label: 'Sort Ascending', - action: function (column) { - this.sortColumn(column.colIndex, 'asc'); - } + columns: [], + data: [], + dropdownButton: '▼', + headerDropdown: [ + { + label: 'Sort Ascending', + action: function (column) { + this.sortColumn(column.colIndex, 'asc'); + } + }, + { + label: 'Sort Descending', + action: function (column) { + this.sortColumn(column.colIndex, 'desc'); + } + }, + { + label: 'Reset sorting', + action: function (column) { + this.sortColumn(column.colIndex, 'none'); + } + }, + { + label: 'Remove column', + action: function (column) { + this.removeColumn(column.colIndex); + } + } + ], + events: { + onRemoveColumn(column) {}, + onSwitchColumn(column1, column2) {}, + onSortColumn(column) {} }, - { - label: 'Sort Descending', - action: function (column) { - this.sortColumn(column.colIndex, 'desc'); - } + sortIndicator: { + asc: '↑', + desc: '↓', + none: '' }, - { - label: 'Reset sorting', - action: function (column) { - this.sortColumn(column.colIndex, 'none'); - } - }, - { - label: 'Remove column', - action: function (column) { - this.removeColumn(column.colIndex); - } - } - ], - events: { - onRemoveColumn(column) {}, - onSwitchColumn(column1, column2) {}, - onSortColumn(column) {} - }, - sortIndicator: { - asc: '↑', - desc: '↓', - none: '' - }, - freezeMessage: '', - getEditor: () => {}, - addSerialNoColumn: true, - addCheckboxColumn: false, - enableClusterize: true, - enableLogs: false, - layout: 'fixed', // fixed, fluid - noDataMessage: 'No Data', - cellHeight: null, - enableInlineFilters: false + freezeMessage: '', + getEditor: () => {}, + addSerialNoColumn: true, + addCheckboxColumn: false, + enableClusterize: true, + enableLogs: false, + layout: 'fixed', // fixed, fluid + noDataMessage: 'No Data', + cellHeight: null, + enableInlineFilters: false }; class DataTable { - constructor(wrapper, options) { - DataTable.instances++; + constructor(wrapper, options) { + DataTable.instances++; - if (typeof wrapper === 'string') { - // css selector - wrapper = document.querySelector(wrapper); - } - this.wrapper = wrapper; - if (!(this.wrapper instanceof HTMLElement)) { - throw new Error('Invalid argument given for `wrapper`'); + if (typeof wrapper === 'string') { + // css selector + wrapper = document.querySelector(wrapper); + } + this.wrapper = wrapper; + if (!(this.wrapper instanceof HTMLElement)) { + throw new Error('Invalid argument given for `wrapper`'); + } + + this.options = Object.assign({}, DEFAULT_OPTIONS, options); + this.options.headerDropdown = + DEFAULT_OPTIONS.headerDropdown + .concat(options.headerDropdown || []); + // custom user events + this.events = Object.assign({}, DEFAULT_OPTIONS.events, options.events || {}); + this.fireEvent = this.fireEvent.bind(this); + + this.prepare(); + + this.style = new Style(this); + this.keyboard = new Keyboard(this.wrapper); + this.datamanager = new DataManager(this.options); + this.rowmanager = new RowManager(this); + this.columnmanager = new ColumnManager(this); + this.cellmanager = new CellManager(this); + this.bodyRenderer = new BodyRenderer(this); + + if (this.options.data) { + this.refresh(); + } } - this.options = Object.assign({}, DEFAULT_OPTIONS, options); - this.options.headerDropdown = - DEFAULT_OPTIONS.headerDropdown - .concat(options.headerDropdown || []); - // custom user events - this.events = Object.assign( - {}, DEFAULT_OPTIONS.events, options.events || {} - ); - this.fireEvent = this.fireEvent.bind(this); - - this.prepare(); - - this.style = new Style(this); - this.keyboard = new Keyboard(this.wrapper); - this.datamanager = new DataManager(this.options); - this.rowmanager = new RowManager(this); - this.columnmanager = new ColumnManager(this); - this.cellmanager = new CellManager(this); - this.bodyRenderer = new BodyRenderer(this); - - if (this.options.data) { - this.refresh(); + prepare() { + this.prepareDom(); + this.unfreeze(); } - } - prepare() { - this.prepareDom(); - this.unfreeze(); - } - - prepareDom() { - this.wrapper.innerHTML = ` + prepareDom() { + this.wrapper.innerHTML = `
@@ -3045,110 +3165,110 @@ class DataTable {
`; - this.datatableWrapper = $('.data-table', this.wrapper); - this.header = $('.data-table-header', this.wrapper); - this.bodyScrollable = $('.body-scrollable', this.wrapper); - this.freezeContainer = $('.freeze-container', this.wrapper); - } - - refresh(data) { - this.datamanager.init(data); - this.render(); - this.setDimensions(); - } - - destroy() { - this.wrapper.innerHTML = ''; - this.style.destroy(); - } - - appendRows(rows) { - this.datamanager.appendRows(rows); - this.rowmanager.refreshRows(); - } - - refreshRow(row, rowIndex) { - this.rowmanager.refreshRow(row, rowIndex); - } - - render() { - this.renderHeader(); - this.renderBody(); - } - - renderHeader() { - this.columnmanager.renderHeader(); - } - - renderBody() { - this.bodyRenderer.render(); - } - - setDimensions() { - this.style.setDimensions(); - } - - getColumn(colIndex) { - return this.datamanager.getColumn(colIndex); - } - - getColumns() { - return this.datamanager.getColumns(); - } - - getRows() { - return this.datamanager.getRows(); - } - - getCell(colIndex, rowIndex) { - return this.datamanager.getCell(colIndex, rowIndex); - } - - getColumnHeaderElement(colIndex) { - return this.columnmanager.getColumnHeaderElement(colIndex); - } - - getViewportHeight() { - if (!this.viewportHeight) { - this.viewportHeight = $.style(this.bodyScrollable, 'height'); + this.datatableWrapper = $('.data-table', this.wrapper); + this.header = $('.data-table-header', this.wrapper); + this.bodyScrollable = $('.body-scrollable', this.wrapper); + this.freezeContainer = $('.freeze-container', this.wrapper); } - return this.viewportHeight; - } - - sortColumn(colIndex, sortOrder) { - this.columnmanager.sortColumn(colIndex, sortOrder); - } - - removeColumn(colIndex) { - this.columnmanager.removeColumn(colIndex); - } - - scrollToLastColumn() { - this.datatableWrapper.scrollLeft = 9999; - } - - freeze() { - $.style(this.freezeContainer, { - display: '' - }); - } - - unfreeze() { - $.style(this.freezeContainer, { - display: 'none' - }); - } - - fireEvent(eventName, ...args) { - this.events[eventName].apply(this, args); - } - - log() { - if (this.options.enableLogs) { - console.log.apply(console, arguments); + refresh(data) { + this.datamanager.init(data); + this.render(); + this.setDimensions(); + } + + destroy() { + this.wrapper.innerHTML = ''; + this.style.destroy(); + } + + appendRows(rows) { + this.datamanager.appendRows(rows); + this.rowmanager.refreshRows(); + } + + refreshRow(row, rowIndex) { + this.rowmanager.refreshRow(row, rowIndex); + } + + render() { + this.renderHeader(); + this.renderBody(); + } + + renderHeader() { + this.columnmanager.renderHeader(); + } + + renderBody() { + this.bodyRenderer.render(); + } + + setDimensions() { + this.style.setDimensions(); + } + + getColumn(colIndex) { + return this.datamanager.getColumn(colIndex); + } + + getColumns() { + return this.datamanager.getColumns(); + } + + getRows() { + return this.datamanager.getRows(); + } + + getCell(colIndex, rowIndex) { + return this.datamanager.getCell(colIndex, rowIndex); + } + + getColumnHeaderElement(colIndex) { + return this.columnmanager.getColumnHeaderElement(colIndex); + } + + getViewportHeight() { + if (!this.viewportHeight) { + this.viewportHeight = $.style(this.bodyScrollable, 'height'); + } + + return this.viewportHeight; + } + + sortColumn(colIndex, sortOrder) { + this.columnmanager.sortColumn(colIndex, sortOrder); + } + + removeColumn(colIndex) { + this.columnmanager.removeColumn(colIndex); + } + + scrollToLastColumn() { + this.datatableWrapper.scrollLeft = 9999; + } + + freeze() { + $.style(this.freezeContainer, { + display: '' + }); + } + + unfreeze() { + $.style(this.freezeContainer, { + display: 'none' + }); + } + + fireEvent(eventName, ...args) { + this.events[eventName].apply(this, args); + } + + log() { + if (this.options.enableLogs) { + console.log.apply(console, arguments); + } } - } } DataTable.instances = 0; diff --git a/dist/frappe-datatable.css b/dist/frappe-datatable.css index 90ab6d1..30dd637 100644 --- a/dist/frappe-datatable.css +++ b/dist/frappe-datatable.css @@ -1,3 +1,4 @@ +/* This file is processed by postcss */ /* variables */ .data-table { diff --git a/dist/frappe-datatable.js b/dist/frappe-datatable.js index b3e7b32..01060a9 100644 --- a/dist/frappe-datatable.js +++ b/dist/frappe-datatable.js @@ -5,175 +5,185 @@ Sortable = Sortable && Sortable.hasOwnProperty('default') ? Sortable['default'] Clusterize = Clusterize && Clusterize.hasOwnProperty('default') ? Clusterize['default'] : Clusterize; function $(expr, con) { - return typeof expr === 'string' ? - (con || document).querySelector(expr) : - expr || null; + 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; + return typeof expr === 'string' ? + Array.from((con || document).querySelectorAll(expr)) : + expr || null; }; $.create = (tag, o) => { - let element = document.createElement(tag); + let element = document.createElement(tag); - for (let i in o) { - let val = o[i]; + 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]; - }); - } + if (i === 'inside') { + $(val).appendChild(element); } else - if (i in element) { + 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 { + } else { element.setAttribute(i, val); - } - } + } + } - return element; + return element; }; $.on = (element, event, selector, callback) => { - if (!callback) { - callback = selector; - $.bind(element, event, callback); - } else { - $.delegate(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); + element.removeEventListener(event, handler); }; $.bind = (element, event, callback) => { - event.split(/\s+/).forEach(function (event) { - element.addEventListener(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); - } - }); + 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]; + if (element) { + for (let event in o) { + let callback = o[event]; - event.split(/\s+/).forEach(function (event) { - element.removeEventListener(event, callback); - }); + event.split(/\s+/).forEach(function (event) { + element.removeEventListener(event, callback); + }); + } } - } }; $.fire = (target, type, properties) => { - let evt = document.createEvent('HTMLEvents'); + let evt = document.createEvent('HTMLEvents'); - evt.initEvent(type, true, true); + evt.initEvent(type, true, true); - for (let j in properties) { - evt[j] = properties[j]; - } + for (let j in properties) { + evt[j] = properties[j]; + } - return target.dispatchEvent(evt); + return target.dispatchEvent(evt); }; $.data = (element, attrs) => { // eslint-disable-line - if (!attrs) { - return element.dataset; - } + if (!attrs) { + return element.dataset; + } - for (const attr in attrs) { - element.dataset[attr] = attrs[attr]; - } + 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]; + 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] = ''; + 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]; + let val = getComputedStyle(element)[prop]; - if (['width', 'height'].includes(prop)) { - val = parseFloat(val); - } + if (['width', 'height'].includes(prop)) { + val = parseFloat(val); + } - return val; + return val; }; $.closest = (selector, element) => { - if (!element) return null; + if (!element) return null; - if (element.matches(selector)) { - return element; - } + if (element.matches(selector)) { + return element; + } - return $.closest(selector, element.parentNode); + 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(); + 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; + return top >= pTop && left >= pLeft && bottom <= pBottom && right <= pRight; }; $.scrollTop = function scrollTop(element, pixels) { - requestAnimationFrame(() => { - element.scrollTop = pixels; - }); + requestAnimationFrame(() => { + element.scrollTop = pixels; + }); }; /** @@ -694,26 +704,26 @@ function throttle(func, wait, options) { var throttle_1 = throttle; function camelCaseToDash(str) { - return str.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); + return str.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); } function makeDataAttributeString(props) { - const keys = Object.keys(props); + const keys = Object.keys(props); - return keys - .map((key) => { - const _key = camelCaseToDash(key); - const val = props[key]; + return keys + .map((key) => { + const _key = camelCaseToDash(key); + const val = props[key]; - if (val === undefined) return ''; - return `data-${_key}="${val}" `; - }) - .join('') - .trim(); + if (val === undefined) return ''; + return `data-${_key}="${val}" `; + }) + .join('') + .trim(); } function getDefault(a, b) { - return a !== undefined ? a : b; + return a !== undefined ? a : b; } @@ -727,62 +737,62 @@ function getDefault(a, b) { function copyTextToClipboard(text) { - // https://stackoverflow.com/a/30810322/5353542 - var textArea = document.createElement('textarea'); + // 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. - // + // + // *** 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; + // 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'; + // 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; + // 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'; + // 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'; + // Avoid flash of white box if rendered for any reason. + textArea.style.background = 'transparent'; - textArea.value = text; + textArea.value = text; - document.body.appendChild(textArea); + document.body.appendChild(textArea); - textArea.select(); + textArea.select(); - try { - document.execCommand('copy'); - } catch (err) { - console.log('Oops, unable to copy'); - } + try { + document.execCommand('copy'); + } catch (err) { + console.log('Oops, unable to copy'); + } - document.body.removeChild(textArea); + document.body.removeChild(textArea); } function isNumeric(val) { - return !isNaN(val); + return !isNaN(val); } let throttle$1 = throttle_1; @@ -790,936 +800,986 @@ 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); - }); - }; + 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); + 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; + 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); } - 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 - }; - - 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(''); + init(data) { + if (!data) { + data = this.options.data; } - } 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(d[col.id]); - } + 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); } - } - return this.prepareRow(row, index); - }); - } + if (this.options.addSerialNoColumn && !this.hasColumnById('_rowIndex')) { + let cell = { + id: '_rowIndex', + content: '', + align: 'center', + editable: false, + resizable: false, + focusable: false, + dropdown: false + }; - validateColumns() { - const columns = this.options.columns; - if (!Array.isArray(columns)) { - throw new DataError('`columns` must be an array'); + this.columns.push(cell); + } } - columns.forEach((column, i) => { - if (typeof column !== 'string' && typeof column !== 'object') { - throw new DataError(`column "${i}" must be a string or an object`); - } - }); - } + 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 + ''; + } + }; - validateData(data) { - if (Array.isArray(data) && - (data.length === 0 || Array.isArray(data[0]) || typeof data[0] === 'object')) { - return true; + 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; + }); } - throw new DataError('`data` must be an array of arrays or objects'); - } - appendRows(rows) { - this.validateData(rows); + prepareCell(content, i) { + const cell = { + content: '', + align: 'left', + sortOrder: 'none', + colIndex: i, + column: this.columns[i] + }; - 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; + if (content !== null && typeof content === 'object') { + // passed as column/header + Object.assign(cell, content); } else { - col.sortOrder = 'none'; + cell.content = content; } - }); - 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; - } + return cell; } - 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; + prepareNumericColumns() { + const row0 = this.getRow(0); + if (!row0) return; + this.columns = this.columns.map((column, i) => { - 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; - }); + const cellValue = row0[i].content; + if (!column.align && cellValue && isNumeric(cellValue)) { + column.align = 'right'; + } - 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; + return column; }); - }); - } - } - - 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; + prepareRows() { + this.validateData(this.data); - return _row; - } + this.rows = this.data.map((d, i) => { + const index = this._getNextRowCount(); - 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); + let row = []; - // mutate object directly - for (let key in options) { - const newVal = options[key]; - if (newVal !== undefined) { - cell[key] = newVal; - } + 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 an object + 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(d[col.id]); + } + } + } + + return this.prepareRow(row, { + rowIndex: index + }); + }); } - return cell; - } + prepareRow(row, props) { + const baseRowCell = { + rowIndex: props.rowIndex + }; - 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; - } + row = row + .map((cell, i) => this.prepareCell(cell, i)) + .map(cell => Object.assign({}, baseRowCell, cell)); - 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()); + // monkey patched in array object + row.meta = props; + return row; } - return columns; - } + validateColumns() { + const columns = this.options.columns; + if (!Array.isArray(columns)) { + throw new DataError('`columns` must be an array'); + } - getStandardColumnCount() { - if (this.options.addCheckboxColumn && this.options.addSerialNoColumn) { - return 2; + columns.forEach((column, i) => { + if (typeof column !== 'string' && typeof column !== 'object') { + throw new DataError(`column "${i}" must be a string or an object`); + } + }); } - if (this.options.addCheckboxColumn || this.options.addSerialNoColumn) { - return 1; + 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'); } - return 0; - } + appendRows(rows) { + this.validateData(rows); - getColumnCount(skipStandardColumns) { - let val = this.columns.length; - - if (skipStandardColumns) { - val = val - this.getStandardColumnCount(); + this.rows = this.rows.concat(this.prepareRows(rows)); } - return val; - } + sortRows(colIndex, sortOrder = 'none') { + colIndex = +colIndex; - getColumn(colIndex) { - colIndex = +colIndex; - return this.columns.find(col => col.colIndex === colIndex); - } + // reset sortOrder and update for colIndex + this.getColumns() + .map(col => { + if (col.colIndex === colIndex) { + col.sortOrder = sortOrder; + } else { + col.sortOrder = 'none'; + } + }); - getRow(rowIndex) { - rowIndex = +rowIndex; - return this.rows.find(row => row[0].rowIndex === rowIndex); - } + this._sortRows(colIndex, sortOrder); + } - getCell(colIndex, rowIndex) { - rowIndex = +rowIndex; - colIndex = +colIndex; - return this.rows.find(row => row[0].rowIndex === rowIndex)[colIndex]; - } + _sortRows(colIndex, sortOrder) { - get() { - return { - columns: this.columns, - rows: this.rows - }; - } + 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; + } + } - hasColumn(name) { - return Boolean(this.columns.find(col => col.content === name)); - } + 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; - hasColumnById(id) { - return Boolean(this.columns.find(col => col.id === id)); - } + 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; + }); - getColumnIndex(name) { - return this.columns.findIndex(col => col.content === name); - } + 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; + }); + }); + } + } - getColumnIndexById(id) { - return this.columns.findIndex(col => col.id === id); - } + reverseArray(array) { + let left = null; + let right = null; + let length = array.length; - getCheckboxHTML() { - return ''; - } + 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; + constructor(instance) { + this.instance = instance; - linkProperties(this, this.instance, [ - 'options', - 'fireEvent', - 'header', - 'datamanager', - 'style', - 'wrapper', - 'rowmanager', - 'bodyScrollable' - ]); + linkProperties(this, this.instance, [ + 'options', + 'fireEvent', + 'header', + 'datamanager', + 'style', + 'wrapper', + 'rowmanager', + 'bodyScrollable' + ]); - this.bindEvents(); - getDropdownHTML = getDropdownHTML.bind(this, this.options.dropdownButton); - } + this.bindEvents(); + getDropdownHTML = getDropdownHTML.bind(this, this.options.dropdownButton); + } - renderHeader() { - this.header.innerHTML = ''; - this.refreshHeader(); - } + renderHeader() { + this.header.innerHTML = ''; + this.refreshHeader(); + } - refreshHeader() { - const columns = this.datamanager.getColumns(); + refreshHeader() { + const columns = this.datamanager.getColumns(); - if (!$('.data-table-col', this.header)) { - // insert html + 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 }); - } + let html = this.rowmanager.getRowHTML(columns, { + isHeader: 1 + }); + if (this.options.enableInlineFilters) { + html += this.rowmanager.getRowHTML(columns, { + isFilter: 1 + }); + } - $('thead', this.header).innerHTML = html; + $('thead', this.header).innerHTML = html; - this.$filterRow = $('.data-table-row[data-is-filter]', this.header); + 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; - } + 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 - }); + $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]; + // refresh sort indicator + const sortIndicator = $('.sort-indicator', $col); + if (sortIndicator) { + sortIndicator.innerHTML = this.options.sortIndicator[column.sortOrder]; + } + }); } - }); + // reset columnMap + this.$columnMap = []; } - // 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; + bindEvents() { + this.bindDropdown(); + this.bindResizeColumn(); + this.bindMoveColumn(); + this.bindFilter(); } - } - bindResizeColumn() { - let isDragging = false; - let $resizingCell, startWidth, startX; + bindDropdown() { + let $activeDropdown; + $.on(this.header, 'click', '.data-table-dropdown-toggle', (e, $button) => { + const $dropdown = $.closest('.data-table-dropdown', $button); - $.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' + if (!$dropdown.classList.contains('is-active')) { + deactivateDropdown(); + $dropdown.classList.add('is-active'); + $activeDropdown = $dropdown; + } else { + deactivateDropdown(); + } }); - }); - 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(document.body, 'click', (e) => { + if (e.target.matches('.data-table-dropdown-toggle')) return; + deactivateDropdown(); }); - }; - $.on(this.header, 'keydown', '.data-table-filter', debounce$2(handler, 300)); - } - sortRows(colIndex, sortOrder) { - return this.datamanager.sortRows(colIndex, sortOrder); - } + const dropdownItems = this.options.headerDropdown; - getColumn(colIndex) { - return this.datamanager.getColumn(colIndex); - } + $.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; - getColumns() { - return this.datamanager.getColumns(); - } + callback && callback.call(this.instance, this.getColumn(colIndex)); + }); - 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; + function deactivateDropdown(e) { + $activeDropdown && $activeDropdown.classList.remove('is-active'); + $activeDropdown = null; + } } - $column.style.width = width + 'px'; - } + bindResizeColumn() { + let isDragging = false; + let $resizingCell, startWidth, startX; - getColumnMinWidth(colIndex) { - colIndex = +colIndex; - return this.getColumn(colIndex).minWidth || 24; - } + $.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); - getFirstColumnIndex() { - if (this.options.addCheckboxColumn && this.options.addSerialNoColumn) { - return 2; + 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); + }); } - if (this.options.addCheckboxColumn || this.options.addSerialNoColumn) { - return 1; + 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); } - return 0; - } + bindSortColumn() { - getHeaderCell$(colIndex) { - return $(`.data-table-col[data-col-index="${colIndex}"]`, this.header); - } + $.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); - getLastColumnIndex() { - return this.datamanager.getColumnCount() - 1; - } + if (col && col.sortable === false) { + return; + } - getSerialColumnIndex() { - const columns = this.datamanager.getColumns(); + // reset sort indicator + $('.sort-indicator', this.header).textContent = ''; + $.each('.data-table-col', this.header).map($cell => { + $.data($cell, { + sortOrder: 'none' + }); + }); - return columns.findIndex(column => column.content.includes('Sr. No')); - } + 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; + // add dropdown buttons + const dropdownItems = this.options.headerDropdown; - return `
${dropdownButton}
+ return `
${dropdownButton}
${dropdownItems.map((d, i) => `
${d.label}
`).join('')}
@@ -1727,603 +1787,649 @@ var getDropdownHTML = function getDropdownHTML(dropdownButton = 'v') { }; class CellManager { - constructor(instance) { - this.instance = instance; - this.wrapper = this.instance.wrapper; - this.options = this.instance.options; - this.style = this.instance.style; - this.bodyScrollable = this.instance.bodyScrollable; - this.columnmanager = this.instance.columnmanager; - this.rowmanager = this.instance.rowmanager; - this.datamanager = this.instance.datamanager; - this.keyboard = this.instance.keyboard; + constructor(instance) { + this.instance = instance; + this.wrapper = this.instance.wrapper; + this.options = this.instance.options; + this.style = this.instance.style; + this.bodyScrollable = this.instance.bodyScrollable; + this.columnmanager = this.instance.columnmanager; + this.rowmanager = this.instance.rowmanager; + this.datamanager = this.instance.datamanager; + this.keyboard = this.instance.keyboard; - this.bindEvents(); - } + this.bindEvents(); + } - bindEvents() { - this.bindFocusCell(); - this.bindEditCell(); - this.bindKeyboardSelection(); - this.bindCopyCellContents(); - this.bindMouseEvents(); - } + bindEvents() { + this.bindFocusCell(); + this.bindEditCell(); + this.bindKeyboardSelection(); + this.bindCopyCellContents(); + this.bindMouseEvents(); + } - bindFocusCell() { - this.bindKeyboardNav(); - } + bindFocusCell() { + this.bindKeyboardNav(); + } - bindEditCell() { - this.$editingCell = null; + bindEditCell() { + this.$editingCell = null; - $.on(this.bodyScrollable, 'dblclick', '.data-table-col', (e, cell) => { - this.activateEditing(cell); - }); + $.on(this.bodyScrollable, 'dblclick', '.data-table-col', (e, cell) => { + this.activateEditing(cell); + }); + + this.keyboard.on('enter', (e) => { + if (this.$focusedCell && !this.$editingCell) { + // enter keypress on focused cell + this.activateEditing(this.$focusedCell); + } else if (this.$editingCell) { + // enter keypress on editing cell + this.submitEditing(); + this.deactivateEditing(); + } + }); + } + + bindKeyboardNav() { + const focusCell = (direction) => { + if (!this.$focusedCell || this.$editingCell) { + return false; + } + + let $cell = this.$focusedCell; + + if (direction === 'left' || direction === 'shift+tab') { + $cell = this.getLeftCell$($cell); + } else if (direction === 'right' || direction === 'tab') { + $cell = this.getRightCell$($cell); + } else if (direction === 'up') { + $cell = this.getAboveCell$($cell); + } else if (direction === 'down') { + $cell = this.getBelowCell$($cell); + } + + this.focusCell($cell); + return true; + }; + + const focusLastCell = (direction) => { + if (!this.$focusedCell || this.$editingCell) { + return false; + } + + let $cell = this.$focusedCell; + const { + rowIndex, + colIndex + } = $.data($cell); + + if (direction === 'left') { + $cell = this.getLeftMostCell$(rowIndex); + } else if (direction === 'right') { + $cell = this.getRightMostCell$(rowIndex); + } else if (direction === 'up') { + $cell = this.getTopMostCell$(colIndex); + } else if (direction === 'down') { + $cell = this.getBottomMostCell$(colIndex); + } + + this.focusCell($cell); + return true; + }; + + ['left', 'right', 'up', 'down', 'tab', 'shift+tab'].map( + direction => this.keyboard.on(direction, () => focusCell(direction)) + ); + + ['left', 'right', 'up', 'down'].map( + direction => this.keyboard.on('ctrl+' + direction, () => focusLastCell(direction)) + ); + + this.keyboard.on('esc', () => { + this.deactivateEditing(); + }); + + if (this.options.enableInlineFilters) { + this.keyboard.on('ctrl+f', (e) => { + const $cell = $.closest('.data-table-col', e.target); + let { + colIndex + } = $.data($cell); + + this.activateFilter(colIndex); + return true; + }); + } + } + + bindKeyboardSelection() { + const getNextSelectionCursor = (direction) => { + let $selectionCursor = this.getSelectionCursor(); + + if (direction === 'left') { + $selectionCursor = this.getLeftCell$($selectionCursor); + } else if (direction === 'right') { + $selectionCursor = this.getRightCell$($selectionCursor); + } else if (direction === 'up') { + $selectionCursor = this.getAboveCell$($selectionCursor); + } else if (direction === 'down') { + $selectionCursor = this.getBelowCell$($selectionCursor); + } + + return $selectionCursor; + }; + + ['left', 'right', 'up', 'down'].map( + direction => this.keyboard.on('shift+' + direction, + () => this.selectArea(getNextSelectionCursor(direction))) + ); + } + + bindCopyCellContents() { + this.keyboard.on('ctrl+c', () => { + this.copyCellContents(this.$focusedCell, this.$selectionCursor); + }); + } + + bindMouseEvents() { + let mouseDown = null; + + $.on(this.bodyScrollable, 'mousedown', '.data-table-col', (e) => { + mouseDown = true; + this.focusCell($(e.delegatedTarget)); + }); + + $.on(this.bodyScrollable, 'mouseup', () => { + mouseDown = false; + }); + + const selectArea = (e) => { + if (!mouseDown) return; + this.selectArea($(e.delegatedTarget)); + }; + + $.on(this.bodyScrollable, 'mousemove', '.data-table-col', throttle$1(selectArea, 50)); + } + + focusCell($cell, { + skipClearSelection = 0 + } = {}) { + if (!$cell) return; + + // don't focus if already editing cell + if ($cell === this.$editingCell) return; + + const { + colIndex, + isHeader + } = $.data($cell); + if (isHeader) { + return; + } + + const column = this.columnmanager.getColumn(colIndex); + if (column.focusable === false) { + return; + } + + this.scrollToCell($cell); - this.keyboard.on('enter', (e) => { - if (this.$focusedCell && !this.$editingCell) { - // enter keypress on focused cell - this.activateEditing(this.$focusedCell); - } else if (this.$editingCell) { - // enter keypress on editing cell - this.submitEditing(); this.deactivateEditing(); - } - }); - } + if (!skipClearSelection) { + this.clearSelection(); + } - bindKeyboardNav() { - const focusCell = (direction) => { - if (!this.$focusedCell || this.$editingCell) { - return false; - } + if (this.$focusedCell) { + this.$focusedCell.classList.remove('selected'); + } - let $cell = this.$focusedCell; + this.$focusedCell = $cell; + $cell.classList.add('selected'); - if (direction === 'left' || direction === 'shift+tab') { - $cell = this.getLeftCell$($cell); - } else if (direction === 'right' || direction === 'tab') { - $cell = this.getRightCell$($cell); - } else if (direction === 'up') { - $cell = this.getAboveCell$($cell); - } else if (direction === 'down') { - $cell = this.getBelowCell$($cell); - } - - this.focusCell($cell); - return true; - }; - - const focusLastCell = (direction) => { - if (!this.$focusedCell || this.$editingCell) { - return false; - } - - let $cell = this.$focusedCell; - const { rowIndex, colIndex } = $.data($cell); - - if (direction === 'left') { - $cell = this.getLeftMostCell$(rowIndex); - } else if (direction === 'right') { - $cell = this.getRightMostCell$(rowIndex); - } else if (direction === 'up') { - $cell = this.getTopMostCell$(colIndex); - } else if (direction === 'down') { - $cell = this.getBottomMostCell$(colIndex); - } - - this.focusCell($cell); - return true; - }; - - ['left', 'right', 'up', 'down', 'tab', 'shift+tab'].map( - direction => this.keyboard.on(direction, () => focusCell(direction)) - ); - - ['left', 'right', 'up', 'down'].map( - direction => this.keyboard.on('ctrl+' + direction, () => focusLastCell(direction)) - ); - - this.keyboard.on('esc', () => { - this.deactivateEditing(); - }); - - if (this.options.enableInlineFilters) { - this.keyboard.on('ctrl+f', (e) => { - const $cell = $.closest('.data-table-col', e.target); - let { colIndex } = $.data($cell); - - this.activateFilter(colIndex); - return true; - }); - } - } - - bindKeyboardSelection() { - const getNextSelectionCursor = (direction) => { - let $selectionCursor = this.getSelectionCursor(); - - if (direction === 'left') { - $selectionCursor = this.getLeftCell$($selectionCursor); - } else if (direction === 'right') { - $selectionCursor = this.getRightCell$($selectionCursor); - } else if (direction === 'up') { - $selectionCursor = this.getAboveCell$($selectionCursor); - } else if (direction === 'down') { - $selectionCursor = this.getBelowCell$($selectionCursor); - } - - return $selectionCursor; - }; - - ['left', 'right', 'up', 'down'].map( - direction => this.keyboard.on('shift+' + direction, - () => this.selectArea(getNextSelectionCursor(direction))) - ); - } - - bindCopyCellContents() { - this.keyboard.on('ctrl+c', () => { - this.copyCellContents(this.$focusedCell, this.$selectionCursor); - }); - } - - bindMouseEvents() { - let mouseDown = null; - - $.on(this.bodyScrollable, 'mousedown', '.data-table-col', (e) => { - mouseDown = true; - this.focusCell($(e.delegatedTarget)); - }); - - $.on(this.bodyScrollable, 'mouseup', () => { - mouseDown = false; - }); - - const selectArea = (e) => { - if (!mouseDown) return; - this.selectArea($(e.delegatedTarget)); - }; - - $.on(this.bodyScrollable, 'mousemove', '.data-table-col', throttle$1(selectArea, 50)); - } - - focusCell($cell, { skipClearSelection = 0 } = {}) { - if (!$cell) return; - - // don't focus if already editing cell - if ($cell === this.$editingCell) return; - - const { colIndex, isHeader } = $.data($cell); - if (isHeader) { - return; - } - - const column = this.columnmanager.getColumn(colIndex); - if (column.focusable === false) { - return; - } - - this.scrollToCell($cell); - - this.deactivateEditing(); - if (!skipClearSelection) { - this.clearSelection(); - } - - if (this.$focusedCell) { - this.$focusedCell.classList.remove('selected'); - } - - this.$focusedCell = $cell; - $cell.classList.add('selected'); - - // so that keyboard nav works - $cell.focus(); - - this.highlightRowColumnHeader($cell); - } - - highlightRowColumnHeader($cell) { - const { colIndex, rowIndex } = $.data($cell); - const _colIndex = this.datamanager.getColumnIndexById('_rowIndex'); - const colHeaderSelector = `.data-table-header .data-table-col[data-col-index="${colIndex}"]`; - const rowHeaderSelector = `.data-table-col[data-row-index="${rowIndex}"][data-col-index="${_colIndex}"]`; - - if (this.lastHeaders) { - $.removeStyle(this.lastHeaders, 'backgroundColor'); - } - - const colHeader = $(colHeaderSelector, this.wrapper); - const rowHeader = $(rowHeaderSelector, this.wrapper); - - $.style([colHeader, rowHeader], { - backgroundColor: '#f5f7fa' // light-bg - }); - - this.lastHeaders = [colHeader, rowHeader]; - } - - selectAreaOnClusterChanged() { - if (!(this.$focusedCell && this.$selectionCursor)) return; - const { colIndex, rowIndex } = $.data(this.$selectionCursor); - const $cell = this.getCell$(colIndex, rowIndex); - - if (!$cell || $cell === this.$selectionCursor) return; - - // selectArea needs $focusedCell - const fCell = $.data(this.$focusedCell); - this.$focusedCell = this.getCell$(fCell.colIndex, fCell.rowIndex); - - this.selectArea($cell); - } - - focusCellOnClusterChanged() { - if (!this.$focusedCell) return; - - const { colIndex, rowIndex } = $.data(this.$focusedCell); - const $cell = this.getCell$(colIndex, rowIndex); - - if (!$cell) return; - // this function is called after selectAreaOnClusterChanged, - // focusCell calls clearSelection which resets the area selection - // so a flag to skip it - this.focusCell($cell, { skipClearSelection: 1 }); - } - - selectArea($selectionCursor) { - if (!this.$focusedCell) return; - - if (this._selectArea(this.$focusedCell, $selectionCursor)) { - // valid selection - this.$selectionCursor = $selectionCursor; - } - }; - - _selectArea($cell1, $cell2) { - if ($cell1 === $cell2) return false; - - const cells = this.getCellsInRange($cell1, $cell2); - if (!cells) return false; - - this.clearSelection(); - cells.map(index => this.getCell$(...index)).map($cell => $cell.classList.add('highlight')); - return true; - } - - getCellsInRange($cell1, $cell2) { - let colIndex1, rowIndex1, colIndex2, rowIndex2; - - if (typeof $cell1 === 'number') { - [colIndex1, rowIndex1, colIndex2, rowIndex2] = arguments; - } else - if (typeof $cell1 === 'object') { - - if (!($cell1 && $cell2)) { - return false; - } - - const cell1 = $.data($cell1); - const cell2 = $.data($cell2); - - colIndex1 = cell1.colIndex; - rowIndex1 = cell1.rowIndex; - colIndex2 = cell2.colIndex; - rowIndex2 = cell2.rowIndex; - } - - if (rowIndex1 > rowIndex2) { - [rowIndex1, rowIndex2] = [rowIndex2, rowIndex1]; - } - - if (colIndex1 > colIndex2) { - [colIndex1, colIndex2] = [colIndex2, colIndex1]; - } - - if (this.isStandardCell(colIndex1) || this.isStandardCell(colIndex2)) { - return false; - } - - let cells = []; - let colIndex = colIndex1; - let rowIndex = rowIndex1; - let rowIndices = []; - - while (rowIndex <= rowIndex2) { - rowIndices.push(rowIndex); - rowIndex++; - } - - rowIndices.map(rowIndex => { - while (colIndex <= colIndex2) { - cells.push([colIndex, rowIndex]); - colIndex++; - } - colIndex = colIndex1; - }); - - return cells; - } - - clearSelection() { - $.each('.data-table-col.highlight', this.bodyScrollable) - .map(cell => cell.classList.remove('highlight')); - - this.$selectionCursor = null; - } - - getSelectionCursor() { - return this.$selectionCursor || this.$focusedCell; - } - - activateEditing($cell) { - this.focusCell($cell); - const { rowIndex, colIndex } = $.data($cell); - - const col = this.columnmanager.getColumn(colIndex); - if (col && (col.editable === false || col.focusable === false)) { - return; - } - - const cell = this.getCell(colIndex, rowIndex); - if (cell && cell.editable === false) { - return; - } - - if (this.$editingCell) { - const { _rowIndex, _colIndex } = $.data(this.$editingCell); - - if (rowIndex === _rowIndex && colIndex === _colIndex) { - // editing the same cell - return; - } - } - - this.$editingCell = $cell; - $cell.classList.add('editing'); - - const $editCell = $('.edit-cell', $cell); - $editCell.innerHTML = ''; - - const editor = this.getEditor(colIndex, rowIndex, cell.content, $editCell); - - if (editor) { - this.currentCellEditor = editor; - // initialize editing input with cell value - editor.initValue(cell.content, rowIndex, col); - } - } - - deactivateEditing() { - // keep focus on the cell so that keyboard navigation works - if (this.$focusedCell) this.$focusedCell.focus(); - - if (!this.$editingCell) return; - this.$editingCell.classList.remove('editing'); - this.$editingCell = null; - } - - getEditor(colIndex, rowIndex, value, parent) { - // debugger; - const obj = this.options.getEditor(colIndex, rowIndex, value, parent); - if (obj && obj.setValue) return obj; - - // editing fallback - const $input = $.create('input', { - class: 'input-style', - type: 'text', - inside: parent - }); - - return { - initValue(value) { - $input.focus(); - $input.value = value; - }, - getValue() { - return $input.value; - }, - setValue(value) { - $input.value = value; - } - }; - } - - submitEditing() { - if (!this.$editingCell) return; - const $cell = this.$editingCell; - const { rowIndex, colIndex } = $.data($cell); - const col = this.datamanager.getColumn(colIndex); - - if ($cell) { - const editor = this.currentCellEditor; - - if (editor) { - const value = editor.getValue(); - const done = editor.setValue(value, rowIndex, col); - const oldValue = this.getCell(colIndex, rowIndex).content; - - // update cell immediately - this.updateCell(colIndex, rowIndex, value); + // so that keyboard nav works $cell.focus(); - if (done && done.then) { - // revert to oldValue if promise fails - done.catch((e) => { - console.log(e); - this.updateCell(colIndex, rowIndex, oldValue); - }); + this.highlightRowColumnHeader($cell); + } + + highlightRowColumnHeader($cell) { + const { + colIndex, + rowIndex + } = $.data($cell); + const _colIndex = this.datamanager.getColumnIndexById('_rowIndex'); + const colHeaderSelector = `.data-table-header .data-table-col[data-col-index="${colIndex}"]`; + const rowHeaderSelector = `.data-table-col[data-row-index="${rowIndex}"][data-col-index="${_colIndex}"]`; + + if (this.lastHeaders) { + $.removeStyle(this.lastHeaders, 'backgroundColor'); } - } + + const colHeader = $(colHeaderSelector, this.wrapper); + const rowHeader = $(rowHeaderSelector, this.wrapper); + + $.style([colHeader, rowHeader], { + backgroundColor: '#f5f7fa' // light-bg + }); + + this.lastHeaders = [colHeader, rowHeader]; } - this.currentCellEditor = null; - } + selectAreaOnClusterChanged() { + if (!(this.$focusedCell && this.$selectionCursor)) return; + const { + colIndex, + rowIndex + } = $.data(this.$selectionCursor); + const $cell = this.getCell$(colIndex, rowIndex); - copyCellContents($cell1, $cell2) { - if (!$cell2 && $cell1) { - // copy only focusedCell - const { colIndex, rowIndex } = $.data($cell1); - const cell = this.getCell(colIndex, rowIndex); - copyTextToClipboard(cell.content); - return; + if (!$cell || $cell === this.$selectionCursor) return; + + // selectArea needs $focusedCell + const fCell = $.data(this.$focusedCell); + this.$focusedCell = this.getCell$(fCell.colIndex, fCell.rowIndex); + + this.selectArea($cell); } - const cells = this.getCellsInRange($cell1, $cell2); - if (!cells) return; + focusCellOnClusterChanged() { + if (!this.$focusedCell) return; - const values = cells - // get cell objects - .map(index => this.getCell(...index)) - // convert to array of rows - .reduce((acc, curr) => { - const rowIndex = curr.rowIndex; + const { + colIndex, + rowIndex + } = $.data(this.$focusedCell); + const $cell = this.getCell$(colIndex, rowIndex); - acc[rowIndex] = acc[rowIndex] || []; - acc[rowIndex].push(curr.content); - - return acc; - }, []) - // join values by tab - .map(row => row.join('\t')) - // join rows by newline - .join('\n'); - - copyTextToClipboard(values); - } - - activateFilter(colIndex) { - this.columnmanager.toggleFilter(); - this.columnmanager.focusFilter(colIndex); - - if (!this.columnmanager.isFilterShown) { - // put focus back on cell - this.$focusedCell.focus(); + if (!$cell) return; + // this function is called after selectAreaOnClusterChanged, + // focusCell calls clearSelection which resets the area selection + // so a flag to skip it + this.focusCell($cell, { + skipClearSelection: 1 + }); } - } - updateCell(colIndex, rowIndex, value) { - const cell = this.datamanager.updateCell(colIndex, rowIndex, { - content: value - }); - this.refreshCell(cell); - } + selectArea($selectionCursor) { + if (!this.$focusedCell) return; - refreshCell(cell) { - const $cell = $(this.cellSelector(cell.colIndex, cell.rowIndex), this.bodyScrollable); - $cell.innerHTML = this.getCellContent(cell); - } + if (this._selectArea(this.$focusedCell, $selectionCursor)) { + // valid selection + this.$selectionCursor = $selectionCursor; + } + }; - isStandardCell(colIndex) { - // Standard cells are in Sr. No and Checkbox column - return colIndex < this.columnmanager.getFirstColumnIndex(); - } + _selectArea($cell1, $cell2) { + if ($cell1 === $cell2) return false; - getCell$(colIndex, rowIndex) { - return $(this.cellSelector(colIndex, rowIndex), this.bodyScrollable); - } + const cells = this.getCellsInRange($cell1, $cell2); + if (!cells) return false; - getAboveCell$($cell) { - const { colIndex } = $.data($cell); - const $aboveRow = $cell.parentElement.previousElementSibling; + this.clearSelection(); + cells.map(index => this.getCell$(...index)).map($cell => $cell.classList.add('highlight')); + return true; + } - return $(`[data-col-index="${colIndex}"]`, $aboveRow); - } + getCellsInRange($cell1, $cell2) { + let colIndex1, rowIndex1, colIndex2, rowIndex2; - getBelowCell$($cell) { - const { colIndex } = $.data($cell); - const $belowRow = $cell.parentElement.nextElementSibling; + if (typeof $cell1 === 'number') { + [colIndex1, rowIndex1, colIndex2, rowIndex2] = arguments; + } else + if (typeof $cell1 === 'object') { - return $(`[data-col-index="${colIndex}"]`, $belowRow); - } + if (!($cell1 && $cell2)) { + return false; + } - getLeftCell$($cell) { - return $cell.previousElementSibling; - } + const cell1 = $.data($cell1); + const cell2 = $.data($cell2); - getRightCell$($cell) { - return $cell.nextElementSibling; - } + colIndex1 = cell1.colIndex; + rowIndex1 = cell1.rowIndex; + colIndex2 = cell2.colIndex; + rowIndex2 = cell2.rowIndex; + } - getLeftMostCell$(rowIndex) { - return this.getCell$(this.columnmanager.getFirstColumnIndex(), rowIndex); - } + if (rowIndex1 > rowIndex2) { + [rowIndex1, rowIndex2] = [rowIndex2, rowIndex1]; + } - getRightMostCell$(rowIndex) { - return this.getCell$(this.columnmanager.getLastColumnIndex(), rowIndex); - } + if (colIndex1 > colIndex2) { + [colIndex1, colIndex2] = [colIndex2, colIndex1]; + } - getTopMostCell$(colIndex) { - return this.getCell$(colIndex, this.rowmanager.getFirstRowIndex()); - } + if (this.isStandardCell(colIndex1) || this.isStandardCell(colIndex2)) { + return false; + } - getBottomMostCell$(colIndex) { - return this.getCell$(colIndex, this.rowmanager.getLastRowIndex()); - } + let cells = []; + let colIndex = colIndex1; + let rowIndex = rowIndex1; + let rowIndices = []; - getCell(colIndex, rowIndex) { - return this.instance.datamanager.getCell(colIndex, rowIndex); - } + while (rowIndex <= rowIndex2) { + rowIndices.push(rowIndex); + rowIndex++; + } - getCellAttr($cell) { - return this.instance.getCellAttr($cell); - } + rowIndices.map(rowIndex => { + while (colIndex <= colIndex2) { + cells.push([colIndex, rowIndex]); + colIndex++; + } + colIndex = colIndex1; + }); - getRowHeight() { - return $.style($('.data-table-row', this.bodyScrollable), 'height'); - } + return cells; + } - scrollToCell($cell) { - if ($.inViewport($cell, this.bodyScrollable)) return false; + clearSelection() { + $.each('.data-table-col.highlight', this.bodyScrollable) + .map(cell => cell.classList.remove('highlight')); - const { rowIndex } = $.data($cell); - this.rowmanager.scrollToRow(rowIndex); - return false; - } + this.$selectionCursor = null; + } - getRowCountPerPage() { - return Math.ceil(this.instance.getViewportHeight() / this.getRowHeight()); - } + getSelectionCursor() { + return this.$selectionCursor || this.$focusedCell; + } - getCellHTML(cell) { - const { rowIndex, colIndex, isHeader, isFilter } = cell; - const dataAttr = makeDataAttributeString({ - rowIndex, - colIndex, - isHeader, - isFilter - }); + activateEditing($cell) { + this.focusCell($cell); + const { + rowIndex, + colIndex + } = $.data($cell); - return ` + const col = this.columnmanager.getColumn(colIndex); + if (col && (col.editable === false || col.focusable === false)) { + return; + } + + const cell = this.getCell(colIndex, rowIndex); + if (cell && cell.editable === false) { + return; + } + + if (this.$editingCell) { + const { + _rowIndex, + _colIndex + } = $.data(this.$editingCell); + + if (rowIndex === _rowIndex && colIndex === _colIndex) { + // editing the same cell + return; + } + } + + this.$editingCell = $cell; + $cell.classList.add('editing'); + + const $editCell = $('.edit-cell', $cell); + $editCell.innerHTML = ''; + + const editor = this.getEditor(colIndex, rowIndex, cell.content, $editCell); + + if (editor) { + this.currentCellEditor = editor; + // initialize editing input with cell value + editor.initValue(cell.content, rowIndex, col); + } + } + + deactivateEditing() { + // keep focus on the cell so that keyboard navigation works + if (this.$focusedCell) this.$focusedCell.focus(); + + if (!this.$editingCell) return; + this.$editingCell.classList.remove('editing'); + this.$editingCell = null; + } + + getEditor(colIndex, rowIndex, value, parent) { + // debugger; + const obj = this.options.getEditor(colIndex, rowIndex, value, parent); + if (obj && obj.setValue) return obj; + + // editing fallback + const $input = $.create('input', { + class: 'input-style', + type: 'text', + inside: parent + }); + + return { + initValue(value) { + $input.focus(); + $input.value = value; + }, + getValue() { + return $input.value; + }, + setValue(value) { + $input.value = value; + } + }; + } + + submitEditing() { + if (!this.$editingCell) return; + const $cell = this.$editingCell; + const { + rowIndex, + colIndex + } = $.data($cell); + const col = this.datamanager.getColumn(colIndex); + + if ($cell) { + const editor = this.currentCellEditor; + + if (editor) { + const value = editor.getValue(); + const done = editor.setValue(value, rowIndex, col); + const oldValue = this.getCell(colIndex, rowIndex).content; + + // update cell immediately + this.updateCell(colIndex, rowIndex, value); + $cell.focus(); + + if (done && done.then) { + // revert to oldValue if promise fails + done.catch((e) => { + console.log(e); + this.updateCell(colIndex, rowIndex, oldValue); + }); + } + } + } + + this.currentCellEditor = null; + } + + copyCellContents($cell1, $cell2) { + if (!$cell2 && $cell1) { + // copy only focusedCell + const { + colIndex, + rowIndex + } = $.data($cell1); + const cell = this.getCell(colIndex, rowIndex); + copyTextToClipboard(cell.content); + return; + } + const cells = this.getCellsInRange($cell1, $cell2); + + if (!cells) return; + + const values = cells + // get cell objects + .map(index => this.getCell(...index)) + // convert to array of rows + .reduce((acc, curr) => { + const rowIndex = curr.rowIndex; + + acc[rowIndex] = acc[rowIndex] || []; + acc[rowIndex].push(curr.content); + + return acc; + }, []) + // join values by tab + .map(row => row.join('\t')) + // join rows by newline + .join('\n'); + + copyTextToClipboard(values); + } + + activateFilter(colIndex) { + this.columnmanager.toggleFilter(); + this.columnmanager.focusFilter(colIndex); + + if (!this.columnmanager.isFilterShown) { + // put focus back on cell + this.$focusedCell.focus(); + } + } + + updateCell(colIndex, rowIndex, value) { + const cell = this.datamanager.updateCell(colIndex, rowIndex, { + content: value + }); + this.refreshCell(cell); + } + + refreshCell(cell) { + const $cell = $(this.cellSelector(cell.colIndex, cell.rowIndex), this.bodyScrollable); + $cell.innerHTML = this.getCellContent(cell); + } + + isStandardCell(colIndex) { + // Standard cells are in Sr. No and Checkbox column + return colIndex < this.columnmanager.getFirstColumnIndex(); + } + + getCell$(colIndex, rowIndex) { + return $(this.cellSelector(colIndex, rowIndex), this.bodyScrollable); + } + + getAboveCell$($cell) { + const { + colIndex + } = $.data($cell); + const $aboveRow = $cell.parentElement.previousElementSibling; + + return $(`[data-col-index="${colIndex}"]`, $aboveRow); + } + + getBelowCell$($cell) { + const { + colIndex + } = $.data($cell); + const $belowRow = $cell.parentElement.nextElementSibling; + + return $(`[data-col-index="${colIndex}"]`, $belowRow); + } + + getLeftCell$($cell) { + return $cell.previousElementSibling; + } + + getRightCell$($cell) { + return $cell.nextElementSibling; + } + + getLeftMostCell$(rowIndex) { + return this.getCell$(this.columnmanager.getFirstColumnIndex(), rowIndex); + } + + getRightMostCell$(rowIndex) { + return this.getCell$(this.columnmanager.getLastColumnIndex(), rowIndex); + } + + getTopMostCell$(colIndex) { + return this.getCell$(colIndex, this.rowmanager.getFirstRowIndex()); + } + + getBottomMostCell$(colIndex) { + return this.getCell$(colIndex, this.rowmanager.getLastRowIndex()); + } + + getCell(colIndex, rowIndex) { + return this.instance.datamanager.getCell(colIndex, rowIndex); + } + + getCellAttr($cell) { + return this.instance.getCellAttr($cell); + } + + getRowHeight() { + return $.style($('.data-table-row', this.bodyScrollable), 'height'); + } + + scrollToCell($cell) { + if ($.inViewport($cell, this.bodyScrollable)) return false; + + const { + rowIndex + } = $.data($cell); + this.rowmanager.scrollToRow(rowIndex); + return false; + } + + getRowCountPerPage() { + return Math.ceil(this.instance.getViewportHeight() / this.getRowHeight()); + } + + getCellHTML(cell) { + const { + rowIndex, + colIndex, + isHeader, + isFilter + } = cell; + const dataAttr = makeDataAttributeString({ + rowIndex, + colIndex, + isHeader, + isFilter + }); + + return ` ${this.getCellContent(cell)} `; - } - - getCellContent(cell) { - const { isHeader } = cell; - - const editable = !isHeader && cell.editable !== false; - const editCellHTML = editable ? this.getEditCellHTML() : ''; - - const sortable = isHeader && cell.sortable !== false; - const sortIndicator = sortable ? '' : ''; - - const resizable = isHeader && cell.resizable !== false; - const resizeColumn = resizable ? '' : ''; - - const hasDropdown = isHeader && cell.dropdown !== false; - const dropdown = hasDropdown ? `
${getDropdownHTML()}
` : ''; - - let contentHTML; - if (cell.isHeader || cell.isFilter || !cell.column.format) { - contentHTML = cell.content; - } else { - contentHTML = cell.column.format(cell.content, cell); } - return ` + getCellContent(cell) { + const { + isHeader + } = cell; + + const editable = !isHeader && cell.editable !== false; + const editCellHTML = editable ? this.getEditCellHTML() : ''; + + const sortable = isHeader && cell.sortable !== false; + const sortIndicator = sortable ? '' : ''; + + const resizable = isHeader && cell.resizable !== false; + const resizeColumn = resizable ? '' : ''; + + const hasDropdown = isHeader && cell.dropdown !== false; + const dropdown = hasDropdown ? `
${getDropdownHTML()}
` : ''; + + let contentHTML; + if (cell.isHeader || cell.isFilter || !cell.column.format) { + contentHTML = cell.content; + } else { + contentHTML = cell.column.format(cell.content, cell); + } + + return `
${(contentHTML)} ${sortIndicator} @@ -2332,321 +2438,332 @@ class CellManager {
${editCellHTML} `; - } + } - getEditCellHTML() { - return ` + getEditCellHTML() { + return `
`; - } + } - cellSelector(colIndex, rowIndex) { - return `.data-table-col[data-col-index="${colIndex}"][data-row-index="${rowIndex}"]`; - } + cellSelector(colIndex, rowIndex) { + return `.data-table-col[data-col-index="${colIndex}"][data-row-index="${rowIndex}"]`; + } } class RowManager { - constructor(instance) { - this.instance = instance; - this.options = this.instance.options; - this.wrapper = this.instance.wrapper; - this.bodyScrollable = this.instance.bodyScrollable; + constructor(instance) { + this.instance = instance; + this.options = this.instance.options; + this.wrapper = this.instance.wrapper; + this.bodyScrollable = this.instance.bodyScrollable; - this.bindEvents(); - this.refreshRows = promisify(this.refreshRows, this); - } - - get datamanager() { - return this.instance.datamanager; - } - - get cellmanager() { - return this.instance.cellmanager; - } - - bindEvents() { - this.bindCheckbox(); - } - - bindCheckbox() { - if (!this.options.addCheckboxColumn) return; - - // map of checked rows - this.checkMap = []; - - $.on(this.wrapper, 'click', '.data-table-col[data-col-index="0"] [type="checkbox"]', (e, $checkbox) => { - const $cell = $checkbox.closest('.data-table-col'); - const { rowIndex, isHeader } = $.data($cell); - const checked = $checkbox.checked; - - if (isHeader) { - this.checkAll(checked); - } else { - this.checkRow(rowIndex, checked); - } - }); - } - - refreshRows() { - this.instance.renderBody(); - this.instance.setDimensions(); - } - - refreshRow(row, rowIndex) { - const _row = this.datamanager.updateRow(row, rowIndex); - - _row.forEach(cell => { - this.cellmanager.refreshCell(cell); - }); - } - - getCheckedRows() { - if (!this.checkMap) { - return []; + this.bindEvents(); + this.refreshRows = promisify(this.refreshRows, this); } - return this.checkMap - .map((c, rowIndex) => { - if (c) { - return rowIndex; + get datamanager() { + return this.instance.datamanager; + } + + get cellmanager() { + return this.instance.cellmanager; + } + + bindEvents() { + this.bindCheckbox(); + } + + bindCheckbox() { + if (!this.options.addCheckboxColumn) return; + + // map of checked rows + this.checkMap = []; + + $.on(this.wrapper, 'click', '.data-table-col[data-col-index="0"] [type="checkbox"]', (e, $checkbox) => { + const $cell = $checkbox.closest('.data-table-col'); + const { + rowIndex, + isHeader + } = $.data($cell); + const checked = $checkbox.checked; + + if (isHeader) { + this.checkAll(checked); + } else { + this.checkRow(rowIndex, checked); + } + }); + } + + refreshRows() { + this.instance.renderBody(); + this.instance.setDimensions(); + } + + refreshRow(row, rowIndex) { + const _row = this.datamanager.updateRow(row, rowIndex); + + _row.forEach(cell => { + this.cellmanager.refreshCell(cell); + }); + } + + getCheckedRows() { + if (!this.checkMap) { + return []; } - return null; - }) - .filter(c => { - return c !== null || c !== undefined; - }); - } - highlightCheckedRows() { - this.getCheckedRows() - .map(rowIndex => this.checkRow(rowIndex, true)); - } - - checkRow(rowIndex, toggle) { - const value = toggle ? 1 : 0; - - // update internal map - this.checkMap[rowIndex] = value; - // set checkbox value explicitly - $.each(`.data-table-col[data-row-index="${rowIndex}"][data-col-index="0"] [type="checkbox"]`, this.bodyScrollable) - .map(input => { - input.checked = toggle; - }); - // highlight row - this.highlightRow(rowIndex, toggle); - } - - checkAll(toggle) { - const value = toggle ? 1 : 0; - - // update internal map - if (toggle) { - this.checkMap = Array.from(Array(this.getTotalRows())).map(c => value); - } else { - this.checkMap = []; - } - // set checkbox value - $.each('.data-table-col[data-col-index="0"] [type="checkbox"]', this.bodyScrollable) - .map(input => { - input.checked = toggle; - }); - // highlight all - this.highlightAll(toggle); - } - - highlightRow(rowIndex, toggle = true) { - const $row = this.getRow$(rowIndex); - if (!$row) return; - - if (!toggle && this.bodyScrollable.classList.contains('row-highlight-all')) { - $row.classList.add('row-unhighlight'); - return; + return this.checkMap + .map((c, rowIndex) => { + if (c) { + return rowIndex; + } + return null; + }) + .filter(c => { + return c !== null || c !== undefined; + }); } - if (toggle && $row.classList.contains('row-unhighlight')) { - $row.classList.remove('row-unhighlight'); + highlightCheckedRows() { + this.getCheckedRows() + .map(rowIndex => this.checkRow(rowIndex, true)); } - this._highlightedRows = this._highlightedRows || {}; - - if (toggle) { - $row.classList.add('row-highlight'); - this._highlightedRows[rowIndex] = $row; - } else { - $row.classList.remove('row-highlight'); - delete this._highlightedRows[rowIndex]; - } - } - - highlightAll(toggle = true) { - if (toggle) { - this.bodyScrollable.classList.add('row-highlight-all'); - } else { - this.bodyScrollable.classList.remove('row-highlight-all'); - for (const rowIndex in this._highlightedRows) { - const $row = this._highlightedRows[rowIndex]; - $row.classList.remove('row-highlight'); - } - this._highlightedRows = {}; - } - } - - getRow$(rowIndex) { - return $(`.data-table-row[data-row-index="${rowIndex}"]`, this.bodyScrollable); - } - - getTotalRows() { - return this.datamanager.getRowCount(); - } - - getFirstRowIndex() { - return 0; - } - - getLastRowIndex() { - return this.datamanager.getRowCount() - 1; - } - - scrollToRow(rowIndex) { - rowIndex = +rowIndex; - this._lastScrollTo = this._lastScrollTo || 0; - const $row = this.getRow$(rowIndex); - if ($.inViewport($row, this.bodyScrollable)) return; - - const { height } = $row.getBoundingClientRect(); - const { top, bottom } = this.bodyScrollable.getBoundingClientRect(); - const rowsInView = Math.floor((bottom - top) / height); - - let offset = 0; - if (rowIndex > this._lastScrollTo) { - offset = height * ((rowIndex + 1) - rowsInView); - } else { - offset = height * ((rowIndex + 1) - 1); + checkRow(rowIndex, toggle) { + const value = toggle ? 1 : 0; + const selector = rowIndex => + `.data-table-col[data-row-index="${rowIndex}"][data-col-index="0"] [type="checkbox"]`; + // update internal map + this.checkMap[rowIndex] = value; + // set checkbox value explicitly + $.each(selector(rowIndex), this.bodyScrollable) + .map(input => { + input.checked = toggle; + }); + // highlight row + this.highlightRow(rowIndex, toggle); } - this._lastScrollTo = rowIndex; - $.scrollTop(this.bodyScrollable, offset); - } + checkAll(toggle) { + const value = toggle ? 1 : 0; - getRowHTML(row, props) { - const dataAttr = makeDataAttributeString(props); - - if (props.isFilter) { - row = row.map(cell => (Object.assign(cell, { - content: this.getFilterInput({ colIndex: cell.colIndex }), - isFilter: 1, - isHeader: undefined, - editable: false - }))); + // update internal map + if (toggle) { + this.checkMap = Array.from(Array(this.getTotalRows())).map(c => value); + } else { + this.checkMap = []; + } + // set checkbox value + $.each('.data-table-col[data-col-index="0"] [type="checkbox"]', this.bodyScrollable) + .map(input => { + input.checked = toggle; + }); + // highlight all + this.highlightAll(toggle); } - return ` - - ${row.map(cell => this.cellmanager.getCellHTML(cell)).join('')} - - `; - } + highlightRow(rowIndex, toggle = true) { + const $row = this.getRow$(rowIndex); + if (!$row) return; - getFilterInput(props) { - const dataAttr = makeDataAttributeString(props); - return ``; - } + if (!toggle && this.bodyScrollable.classList.contains('row-highlight-all')) { + $row.classList.add('row-unhighlight'); + return; + } + + if (toggle && $row.classList.contains('row-unhighlight')) { + $row.classList.remove('row-unhighlight'); + } + + this._highlightedRows = this._highlightedRows || {}; + + if (toggle) { + $row.classList.add('row-highlight'); + this._highlightedRows[rowIndex] = $row; + } else { + $row.classList.remove('row-highlight'); + delete this._highlightedRows[rowIndex]; + } + } + + highlightAll(toggle = true) { + if (toggle) { + this.bodyScrollable.classList.add('row-highlight-all'); + } else { + this.bodyScrollable.classList.remove('row-highlight-all'); + for (const rowIndex in this._highlightedRows) { + const $row = this._highlightedRows[rowIndex]; + $row.classList.remove('row-highlight'); + } + this._highlightedRows = {}; + } + } + + getRow$(rowIndex) { + return $(`.data-table-row[data-row-index="${rowIndex}"]`, this.bodyScrollable); + } + + getTotalRows() { + return this.datamanager.getRowCount(); + } + + getFirstRowIndex() { + return 0; + } + + getLastRowIndex() { + return this.datamanager.getRowCount() - 1; + } + + scrollToRow(rowIndex) { + rowIndex = +rowIndex; + this._lastScrollTo = this._lastScrollTo || 0; + const $row = this.getRow$(rowIndex); + if ($.inViewport($row, this.bodyScrollable)) return; + + const { + height + } = $row.getBoundingClientRect(); + const { + top, + bottom + } = this.bodyScrollable.getBoundingClientRect(); + const rowsInView = Math.floor((bottom - top) / height); + + let offset = 0; + if (rowIndex > this._lastScrollTo) { + offset = height * ((rowIndex + 1) - rowsInView); + } else { + offset = height * ((rowIndex + 1) - 1); + } + + this._lastScrollTo = rowIndex; + $.scrollTop(this.bodyScrollable, offset); + } + + getRowHTML(row, props) { + const dataAttr = makeDataAttributeString(props); + + if (props.isFilter) { + row = row.map(cell => (Object.assign(cell, { + content: this.getFilterInput({ + colIndex: cell.colIndex + }), + isFilter: 1, + isHeader: undefined, + editable: false + }))); + } + + return ` + + ${row.map(cell => this.cellmanager.getCellHTML(cell)).join('')} + + `; + } + + getFilterInput(props) { + const dataAttr = makeDataAttributeString(props); + return ``; + } } class BodyRenderer { - constructor(instance) { - this.instance = instance; - this.options = instance.options; - this.datamanager = instance.datamanager; - this.rowmanager = instance.rowmanager; - this.cellmanager = instance.cellmanager; - this.bodyScrollable = instance.bodyScrollable; - this.log = instance.log; - this.appendRemainingData = promisify(this.appendRemainingData, this); - } - - render() { - if (this.options.enableClusterize) { - this.renderBodyWithClusterize(); - } else { - this.renderBodyHTML(); - } - } - - renderBodyHTML() { - const rows = this.datamanager.getRows(); - - this.bodyScrollable.innerHTML = ` - - ${getBodyHTML(rows)} -
- `; - this.instance.setDimensions(); - this.restoreState(); - } - - renderBodyWithClusterize() { - // first page - const rows = this.datamanager.getRows(0, 20); - const initialData = this.getDataForClusterize(rows); - - if (!this.clusterize) { - // empty body - this.bodyScrollable.innerHTML = ` - - ${getBodyHTML([])} -
- `; - - // first 20 rows will appended - // rest of them in nextTick - this.clusterize = new Clusterize({ - rows: initialData, - scrollElem: this.bodyScrollable, - contentElem: $('tbody', this.bodyScrollable), - callbacks: { - clusterChanged: () => { - this.restoreState(); - } - }, - /* eslint-disable */ - no_data_text: this.options.noDataMessage, - no_data_class: 'empty-state' - /* eslint-enable */ - }); - - // setDimensions requires atleast 1 row to exist in dom - this.instance.setDimensions(); - } else { - this.clusterize.update(initialData); + constructor(instance) { + this.instance = instance; + this.options = instance.options; + this.datamanager = instance.datamanager; + this.rowmanager = instance.rowmanager; + this.cellmanager = instance.cellmanager; + this.bodyScrollable = instance.bodyScrollable; + this.log = instance.log; + this.appendRemainingData = promisify(this.appendRemainingData, this); } - this.appendRemainingData(); - } + render() { + if (this.options.enableClusterize) { + this.renderBodyWithClusterize(); + } else { + this.renderBodyHTML(); + } + } - restoreState() { - this.rowmanager.highlightCheckedRows(); - this.cellmanager.selectAreaOnClusterChanged(); - this.cellmanager.focusCellOnClusterChanged(); - } + renderBodyHTML() { + const rows = this.datamanager.getRows(); - appendRemainingData() { - const rows = this.datamanager.getRows(20); - const data = this.getDataForClusterize(rows); - this.clusterize.append(data); - } + this.bodyScrollable.innerHTML = ` + + ${this.getBodyHTML(rows)} +
+ `; + this.instance.setDimensions(); + this.restoreState(); + } - getDataForClusterize(rows) { - return rows.map((row) => this.rowmanager.getRowHTML(row, { rowIndex: row[0].rowIndex })); - } -} + renderBodyWithClusterize() { + // first page + const rows = this.datamanager.getRows(0, 20); + const initialData = this.getDataForClusterize(rows); -function getBodyHTML(rows) { - return ` - - ${rows.map(row => this.rowmanager.getRowHTML(row, { rowIndex: row[0].rowIndex })).join('')} - - `; + if (!this.clusterize) { + // empty body + this.bodyScrollable.innerHTML = ` + + ${this.getBodyHTML([])} +
+ `; + + // first 20 rows will appended + // rest of them in nextTick + this.clusterize = new Clusterize({ + rows: initialData, + scrollElem: this.bodyScrollable, + contentElem: $('tbody', this.bodyScrollable), + callbacks: { + clusterChanged: () => { + this.restoreState(); + } + }, + /* eslint-disable */ + no_data_text: this.options.noDataMessage, + no_data_class: 'empty-state' + /* eslint-enable */ + }); + + // setDimensions requires atleast 1 row to exist in dom + this.instance.setDimensions(); + } else { + this.clusterize.update(initialData); + } + + this.appendRemainingData(); + } + + restoreState() { + this.rowmanager.highlightCheckedRows(); + this.cellmanager.selectAreaOnClusterChanged(); + this.cellmanager.focusCellOnClusterChanged(); + } + + appendRemainingData() { + const rows = this.datamanager.getRows(20); + const data = this.getDataForClusterize(rows); + this.clusterize.append(data); + } + + getDataForClusterize(rows) { + return rows.map((row) => this.rowmanager.getRowHTML(row, row.meta)); + } + + getBodyHTML(rows) { + return ` + + ${rows.map(row => this.rowmanager.getRowHTML(row, row.meta)).join('')} + + `; + } } class Style { @@ -2683,7 +2800,7 @@ class Style { this.styleEl.remove(); } - setStyle(rule, styleMap, index = -1) { + setStyle(selector, styleMap, index = -1) { const styles = Object.keys(styleMap) .map(prop => { if (!prop.includes('-')) { @@ -2692,7 +2809,12 @@ class Style { return `${prop}:${styleMap[prop]};`; }) .join(''); - let ruleString = `.${this.scopeClass} ${rule} { ${styles} }`; + let prefixedSelector = selector + .split(',') + .map(r => `.${this.scopeClass} ${r}`) + .join(','); + + let ruleString = `${prefixedSelector} { ${styles} }`; let _index = this.styleSheet.cssRules.length; if (index !== -1) { @@ -2880,157 +3002,155 @@ class Style { } const KEYCODES = { - 13: 'enter', - 91: 'meta', - 16: 'shift', - 17: 'ctrl', - 18: 'alt', - 37: 'left', - 38: 'up', - 39: 'right', - 40: 'down', - 9: 'tab', - 27: 'esc', - 67: 'c', - 70: 'f' + 13: 'enter', + 91: 'meta', + 16: 'shift', + 17: 'ctrl', + 18: 'alt', + 37: 'left', + 38: 'up', + 39: 'right', + 40: 'down', + 9: 'tab', + 27: 'esc', + 67: 'c', + 70: 'f' }; class Keyboard { - constructor(element) { - this.listeners = {}; - $.on(element, 'keydown', this.handler.bind(this)); - } - - handler(e) { - let key = KEYCODES[e.keyCode]; - - if (e.shiftKey && key !== 'shift') { - key = 'shift+' + key; + constructor(element) { + this.listeners = {}; + $.on(element, 'keydown', this.handler.bind(this)); } - if ((e.ctrlKey && key !== 'ctrl') || (e.metaKey && key !== 'meta')) { - key = 'ctrl+' + key; - } + handler(e) { + let key = KEYCODES[e.keyCode]; - const listeners = this.listeners[key]; - - if (listeners && listeners.length > 0) { - for (let listener of listeners) { - const preventBubbling = listener(e); - if (preventBubbling === undefined || preventBubbling === true) { - e.preventDefault(); + if (e.shiftKey && key !== 'shift') { + key = 'shift+' + key; + } + + if ((e.ctrlKey && key !== 'ctrl') || (e.metaKey && key !== 'meta')) { + key = 'ctrl+' + key; + } + + const listeners = this.listeners[key]; + + if (listeners && listeners.length > 0) { + for (let listener of listeners) { + const preventBubbling = listener(e); + if (preventBubbling === undefined || preventBubbling === true) { + e.preventDefault(); + } + } } - } } - } - on(key, listener) { - const keys = key.split(',').map(k => k.trim()); + on(key, listener) { + const keys = key.split(',').map(k => k.trim()); - keys.map(key => { - this.listeners[key] = this.listeners[key] || []; - this.listeners[key].push(listener); - }); - } + keys.map(key => { + this.listeners[key] = this.listeners[key] || []; + this.listeners[key].push(listener); + }); + } } var DEFAULT_OPTIONS = { - columns: [], - data: [], - dropdownButton: '▼', - headerDropdown: [ - { - label: 'Sort Ascending', - action: function (column) { - this.sortColumn(column.colIndex, 'asc'); - } + columns: [], + data: [], + dropdownButton: '▼', + headerDropdown: [ + { + label: 'Sort Ascending', + action: function (column) { + this.sortColumn(column.colIndex, 'asc'); + } + }, + { + label: 'Sort Descending', + action: function (column) { + this.sortColumn(column.colIndex, 'desc'); + } + }, + { + label: 'Reset sorting', + action: function (column) { + this.sortColumn(column.colIndex, 'none'); + } + }, + { + label: 'Remove column', + action: function (column) { + this.removeColumn(column.colIndex); + } + } + ], + events: { + onRemoveColumn(column) {}, + onSwitchColumn(column1, column2) {}, + onSortColumn(column) {} }, - { - label: 'Sort Descending', - action: function (column) { - this.sortColumn(column.colIndex, 'desc'); - } + sortIndicator: { + asc: '↑', + desc: '↓', + none: '' }, - { - label: 'Reset sorting', - action: function (column) { - this.sortColumn(column.colIndex, 'none'); - } - }, - { - label: 'Remove column', - action: function (column) { - this.removeColumn(column.colIndex); - } - } - ], - events: { - onRemoveColumn(column) {}, - onSwitchColumn(column1, column2) {}, - onSortColumn(column) {} - }, - sortIndicator: { - asc: '↑', - desc: '↓', - none: '' - }, - freezeMessage: '', - getEditor: () => {}, - addSerialNoColumn: true, - addCheckboxColumn: false, - enableClusterize: true, - enableLogs: false, - layout: 'fixed', // fixed, fluid - noDataMessage: 'No Data', - cellHeight: null, - enableInlineFilters: false + freezeMessage: '', + getEditor: () => {}, + addSerialNoColumn: true, + addCheckboxColumn: false, + enableClusterize: true, + enableLogs: false, + layout: 'fixed', // fixed, fluid + noDataMessage: 'No Data', + cellHeight: null, + enableInlineFilters: false }; class DataTable { - constructor(wrapper, options) { - DataTable.instances++; + constructor(wrapper, options) { + DataTable.instances++; - if (typeof wrapper === 'string') { - // css selector - wrapper = document.querySelector(wrapper); - } - this.wrapper = wrapper; - if (!(this.wrapper instanceof HTMLElement)) { - throw new Error('Invalid argument given for `wrapper`'); + if (typeof wrapper === 'string') { + // css selector + wrapper = document.querySelector(wrapper); + } + this.wrapper = wrapper; + if (!(this.wrapper instanceof HTMLElement)) { + throw new Error('Invalid argument given for `wrapper`'); + } + + this.options = Object.assign({}, DEFAULT_OPTIONS, options); + this.options.headerDropdown = + DEFAULT_OPTIONS.headerDropdown + .concat(options.headerDropdown || []); + // custom user events + this.events = Object.assign({}, DEFAULT_OPTIONS.events, options.events || {}); + this.fireEvent = this.fireEvent.bind(this); + + this.prepare(); + + this.style = new Style(this); + this.keyboard = new Keyboard(this.wrapper); + this.datamanager = new DataManager(this.options); + this.rowmanager = new RowManager(this); + this.columnmanager = new ColumnManager(this); + this.cellmanager = new CellManager(this); + this.bodyRenderer = new BodyRenderer(this); + + if (this.options.data) { + this.refresh(); + } } - this.options = Object.assign({}, DEFAULT_OPTIONS, options); - this.options.headerDropdown = - DEFAULT_OPTIONS.headerDropdown - .concat(options.headerDropdown || []); - // custom user events - this.events = Object.assign( - {}, DEFAULT_OPTIONS.events, options.events || {} - ); - this.fireEvent = this.fireEvent.bind(this); - - this.prepare(); - - this.style = new Style(this); - this.keyboard = new Keyboard(this.wrapper); - this.datamanager = new DataManager(this.options); - this.rowmanager = new RowManager(this); - this.columnmanager = new ColumnManager(this); - this.cellmanager = new CellManager(this); - this.bodyRenderer = new BodyRenderer(this); - - if (this.options.data) { - this.refresh(); + prepare() { + this.prepareDom(); + this.unfreeze(); } - } - prepare() { - this.prepareDom(); - this.unfreeze(); - } - - prepareDom() { - this.wrapper.innerHTML = ` + prepareDom() { + this.wrapper.innerHTML = `
@@ -3044,110 +3164,110 @@ class DataTable {
`; - this.datatableWrapper = $('.data-table', this.wrapper); - this.header = $('.data-table-header', this.wrapper); - this.bodyScrollable = $('.body-scrollable', this.wrapper); - this.freezeContainer = $('.freeze-container', this.wrapper); - } - - refresh(data) { - this.datamanager.init(data); - this.render(); - this.setDimensions(); - } - - destroy() { - this.wrapper.innerHTML = ''; - this.style.destroy(); - } - - appendRows(rows) { - this.datamanager.appendRows(rows); - this.rowmanager.refreshRows(); - } - - refreshRow(row, rowIndex) { - this.rowmanager.refreshRow(row, rowIndex); - } - - render() { - this.renderHeader(); - this.renderBody(); - } - - renderHeader() { - this.columnmanager.renderHeader(); - } - - renderBody() { - this.bodyRenderer.render(); - } - - setDimensions() { - this.style.setDimensions(); - } - - getColumn(colIndex) { - return this.datamanager.getColumn(colIndex); - } - - getColumns() { - return this.datamanager.getColumns(); - } - - getRows() { - return this.datamanager.getRows(); - } - - getCell(colIndex, rowIndex) { - return this.datamanager.getCell(colIndex, rowIndex); - } - - getColumnHeaderElement(colIndex) { - return this.columnmanager.getColumnHeaderElement(colIndex); - } - - getViewportHeight() { - if (!this.viewportHeight) { - this.viewportHeight = $.style(this.bodyScrollable, 'height'); + this.datatableWrapper = $('.data-table', this.wrapper); + this.header = $('.data-table-header', this.wrapper); + this.bodyScrollable = $('.body-scrollable', this.wrapper); + this.freezeContainer = $('.freeze-container', this.wrapper); } - return this.viewportHeight; - } - - sortColumn(colIndex, sortOrder) { - this.columnmanager.sortColumn(colIndex, sortOrder); - } - - removeColumn(colIndex) { - this.columnmanager.removeColumn(colIndex); - } - - scrollToLastColumn() { - this.datatableWrapper.scrollLeft = 9999; - } - - freeze() { - $.style(this.freezeContainer, { - display: '' - }); - } - - unfreeze() { - $.style(this.freezeContainer, { - display: 'none' - }); - } - - fireEvent(eventName, ...args) { - this.events[eventName].apply(this, args); - } - - log() { - if (this.options.enableLogs) { - console.log.apply(console, arguments); + refresh(data) { + this.datamanager.init(data); + this.render(); + this.setDimensions(); + } + + destroy() { + this.wrapper.innerHTML = ''; + this.style.destroy(); + } + + appendRows(rows) { + this.datamanager.appendRows(rows); + this.rowmanager.refreshRows(); + } + + refreshRow(row, rowIndex) { + this.rowmanager.refreshRow(row, rowIndex); + } + + render() { + this.renderHeader(); + this.renderBody(); + } + + renderHeader() { + this.columnmanager.renderHeader(); + } + + renderBody() { + this.bodyRenderer.render(); + } + + setDimensions() { + this.style.setDimensions(); + } + + getColumn(colIndex) { + return this.datamanager.getColumn(colIndex); + } + + getColumns() { + return this.datamanager.getColumns(); + } + + getRows() { + return this.datamanager.getRows(); + } + + getCell(colIndex, rowIndex) { + return this.datamanager.getCell(colIndex, rowIndex); + } + + getColumnHeaderElement(colIndex) { + return this.columnmanager.getColumnHeaderElement(colIndex); + } + + getViewportHeight() { + if (!this.viewportHeight) { + this.viewportHeight = $.style(this.bodyScrollable, 'height'); + } + + return this.viewportHeight; + } + + sortColumn(colIndex, sortOrder) { + this.columnmanager.sortColumn(colIndex, sortOrder); + } + + removeColumn(colIndex) { + this.columnmanager.removeColumn(colIndex); + } + + scrollToLastColumn() { + this.datatableWrapper.scrollLeft = 9999; + } + + freeze() { + $.style(this.freezeContainer, { + display: '' + }); + } + + unfreeze() { + $.style(this.freezeContainer, { + display: 'none' + }); + } + + fireEvent(eventName, ...args) { + this.events[eventName].apply(this, args); + } + + log() { + if (this.options.enableLogs) { + console.log.apply(console, arguments); + } } - } } DataTable.instances = 0; diff --git a/docs/frappe-datatable.css b/docs/frappe-datatable.css index 90ab6d1..30dd637 100644 --- a/docs/frappe-datatable.css +++ b/docs/frappe-datatable.css @@ -1,3 +1,4 @@ +/* This file is processed by postcss */ /* variables */ .data-table { diff --git a/docs/frappe-datatable.js b/docs/frappe-datatable.js index b3e7b32..01060a9 100644 --- a/docs/frappe-datatable.js +++ b/docs/frappe-datatable.js @@ -5,175 +5,185 @@ Sortable = Sortable && Sortable.hasOwnProperty('default') ? Sortable['default'] Clusterize = Clusterize && Clusterize.hasOwnProperty('default') ? Clusterize['default'] : Clusterize; function $(expr, con) { - return typeof expr === 'string' ? - (con || document).querySelector(expr) : - expr || null; + 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; + return typeof expr === 'string' ? + Array.from((con || document).querySelectorAll(expr)) : + expr || null; }; $.create = (tag, o) => { - let element = document.createElement(tag); + let element = document.createElement(tag); - for (let i in o) { - let val = o[i]; + 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]; - }); - } + if (i === 'inside') { + $(val).appendChild(element); } else - if (i in element) { + 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 { + } else { element.setAttribute(i, val); - } - } + } + } - return element; + return element; }; $.on = (element, event, selector, callback) => { - if (!callback) { - callback = selector; - $.bind(element, event, callback); - } else { - $.delegate(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); + element.removeEventListener(event, handler); }; $.bind = (element, event, callback) => { - event.split(/\s+/).forEach(function (event) { - element.addEventListener(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); - } - }); + 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]; + if (element) { + for (let event in o) { + let callback = o[event]; - event.split(/\s+/).forEach(function (event) { - element.removeEventListener(event, callback); - }); + event.split(/\s+/).forEach(function (event) { + element.removeEventListener(event, callback); + }); + } } - } }; $.fire = (target, type, properties) => { - let evt = document.createEvent('HTMLEvents'); + let evt = document.createEvent('HTMLEvents'); - evt.initEvent(type, true, true); + evt.initEvent(type, true, true); - for (let j in properties) { - evt[j] = properties[j]; - } + for (let j in properties) { + evt[j] = properties[j]; + } - return target.dispatchEvent(evt); + return target.dispatchEvent(evt); }; $.data = (element, attrs) => { // eslint-disable-line - if (!attrs) { - return element.dataset; - } + if (!attrs) { + return element.dataset; + } - for (const attr in attrs) { - element.dataset[attr] = attrs[attr]; - } + 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]; + 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] = ''; + 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]; + let val = getComputedStyle(element)[prop]; - if (['width', 'height'].includes(prop)) { - val = parseFloat(val); - } + if (['width', 'height'].includes(prop)) { + val = parseFloat(val); + } - return val; + return val; }; $.closest = (selector, element) => { - if (!element) return null; + if (!element) return null; - if (element.matches(selector)) { - return element; - } + if (element.matches(selector)) { + return element; + } - return $.closest(selector, element.parentNode); + 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(); + 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; + return top >= pTop && left >= pLeft && bottom <= pBottom && right <= pRight; }; $.scrollTop = function scrollTop(element, pixels) { - requestAnimationFrame(() => { - element.scrollTop = pixels; - }); + requestAnimationFrame(() => { + element.scrollTop = pixels; + }); }; /** @@ -694,26 +704,26 @@ function throttle(func, wait, options) { var throttle_1 = throttle; function camelCaseToDash(str) { - return str.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); + return str.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); } function makeDataAttributeString(props) { - const keys = Object.keys(props); + const keys = Object.keys(props); - return keys - .map((key) => { - const _key = camelCaseToDash(key); - const val = props[key]; + return keys + .map((key) => { + const _key = camelCaseToDash(key); + const val = props[key]; - if (val === undefined) return ''; - return `data-${_key}="${val}" `; - }) - .join('') - .trim(); + if (val === undefined) return ''; + return `data-${_key}="${val}" `; + }) + .join('') + .trim(); } function getDefault(a, b) { - return a !== undefined ? a : b; + return a !== undefined ? a : b; } @@ -727,62 +737,62 @@ function getDefault(a, b) { function copyTextToClipboard(text) { - // https://stackoverflow.com/a/30810322/5353542 - var textArea = document.createElement('textarea'); + // 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. - // + // + // *** 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; + // 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'; + // 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; + // 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'; + // 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'; + // Avoid flash of white box if rendered for any reason. + textArea.style.background = 'transparent'; - textArea.value = text; + textArea.value = text; - document.body.appendChild(textArea); + document.body.appendChild(textArea); - textArea.select(); + textArea.select(); - try { - document.execCommand('copy'); - } catch (err) { - console.log('Oops, unable to copy'); - } + try { + document.execCommand('copy'); + } catch (err) { + console.log('Oops, unable to copy'); + } - document.body.removeChild(textArea); + document.body.removeChild(textArea); } function isNumeric(val) { - return !isNaN(val); + return !isNaN(val); } let throttle$1 = throttle_1; @@ -790,936 +800,986 @@ 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); - }); - }; + 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); + 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; + 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); } - 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 - }; - - 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(''); + init(data) { + if (!data) { + data = this.options.data; } - } 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(d[col.id]); - } + 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); } - } - return this.prepareRow(row, index); - }); - } + if (this.options.addSerialNoColumn && !this.hasColumnById('_rowIndex')) { + let cell = { + id: '_rowIndex', + content: '', + align: 'center', + editable: false, + resizable: false, + focusable: false, + dropdown: false + }; - validateColumns() { - const columns = this.options.columns; - if (!Array.isArray(columns)) { - throw new DataError('`columns` must be an array'); + this.columns.push(cell); + } } - columns.forEach((column, i) => { - if (typeof column !== 'string' && typeof column !== 'object') { - throw new DataError(`column "${i}" must be a string or an object`); - } - }); - } + 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 + ''; + } + }; - validateData(data) { - if (Array.isArray(data) && - (data.length === 0 || Array.isArray(data[0]) || typeof data[0] === 'object')) { - return true; + 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; + }); } - throw new DataError('`data` must be an array of arrays or objects'); - } - appendRows(rows) { - this.validateData(rows); + prepareCell(content, i) { + const cell = { + content: '', + align: 'left', + sortOrder: 'none', + colIndex: i, + column: this.columns[i] + }; - 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; + if (content !== null && typeof content === 'object') { + // passed as column/header + Object.assign(cell, content); } else { - col.sortOrder = 'none'; + cell.content = content; } - }); - 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; - } + return cell; } - 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; + prepareNumericColumns() { + const row0 = this.getRow(0); + if (!row0) return; + this.columns = this.columns.map((column, i) => { - 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; - }); + const cellValue = row0[i].content; + if (!column.align && cellValue && isNumeric(cellValue)) { + column.align = 'right'; + } - 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; + return column; }); - }); - } - } - - 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; + prepareRows() { + this.validateData(this.data); - return _row; - } + this.rows = this.data.map((d, i) => { + const index = this._getNextRowCount(); - 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); + let row = []; - // mutate object directly - for (let key in options) { - const newVal = options[key]; - if (newVal !== undefined) { - cell[key] = newVal; - } + 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 an object + 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(d[col.id]); + } + } + } + + return this.prepareRow(row, { + rowIndex: index + }); + }); } - return cell; - } + prepareRow(row, props) { + const baseRowCell = { + rowIndex: props.rowIndex + }; - 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; - } + row = row + .map((cell, i) => this.prepareCell(cell, i)) + .map(cell => Object.assign({}, baseRowCell, cell)); - 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()); + // monkey patched in array object + row.meta = props; + return row; } - return columns; - } + validateColumns() { + const columns = this.options.columns; + if (!Array.isArray(columns)) { + throw new DataError('`columns` must be an array'); + } - getStandardColumnCount() { - if (this.options.addCheckboxColumn && this.options.addSerialNoColumn) { - return 2; + columns.forEach((column, i) => { + if (typeof column !== 'string' && typeof column !== 'object') { + throw new DataError(`column "${i}" must be a string or an object`); + } + }); } - if (this.options.addCheckboxColumn || this.options.addSerialNoColumn) { - return 1; + 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'); } - return 0; - } + appendRows(rows) { + this.validateData(rows); - getColumnCount(skipStandardColumns) { - let val = this.columns.length; - - if (skipStandardColumns) { - val = val - this.getStandardColumnCount(); + this.rows = this.rows.concat(this.prepareRows(rows)); } - return val; - } + sortRows(colIndex, sortOrder = 'none') { + colIndex = +colIndex; - getColumn(colIndex) { - colIndex = +colIndex; - return this.columns.find(col => col.colIndex === colIndex); - } + // reset sortOrder and update for colIndex + this.getColumns() + .map(col => { + if (col.colIndex === colIndex) { + col.sortOrder = sortOrder; + } else { + col.sortOrder = 'none'; + } + }); - getRow(rowIndex) { - rowIndex = +rowIndex; - return this.rows.find(row => row[0].rowIndex === rowIndex); - } + this._sortRows(colIndex, sortOrder); + } - getCell(colIndex, rowIndex) { - rowIndex = +rowIndex; - colIndex = +colIndex; - return this.rows.find(row => row[0].rowIndex === rowIndex)[colIndex]; - } + _sortRows(colIndex, sortOrder) { - get() { - return { - columns: this.columns, - rows: this.rows - }; - } + 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; + } + } - hasColumn(name) { - return Boolean(this.columns.find(col => col.content === name)); - } + 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; - hasColumnById(id) { - return Boolean(this.columns.find(col => col.id === id)); - } + 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; + }); - getColumnIndex(name) { - return this.columns.findIndex(col => col.content === name); - } + 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; + }); + }); + } + } - getColumnIndexById(id) { - return this.columns.findIndex(col => col.id === id); - } + reverseArray(array) { + let left = null; + let right = null; + let length = array.length; - getCheckboxHTML() { - return ''; - } + 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; + constructor(instance) { + this.instance = instance; - linkProperties(this, this.instance, [ - 'options', - 'fireEvent', - 'header', - 'datamanager', - 'style', - 'wrapper', - 'rowmanager', - 'bodyScrollable' - ]); + linkProperties(this, this.instance, [ + 'options', + 'fireEvent', + 'header', + 'datamanager', + 'style', + 'wrapper', + 'rowmanager', + 'bodyScrollable' + ]); - this.bindEvents(); - getDropdownHTML = getDropdownHTML.bind(this, this.options.dropdownButton); - } + this.bindEvents(); + getDropdownHTML = getDropdownHTML.bind(this, this.options.dropdownButton); + } - renderHeader() { - this.header.innerHTML = ''; - this.refreshHeader(); - } + renderHeader() { + this.header.innerHTML = ''; + this.refreshHeader(); + } - refreshHeader() { - const columns = this.datamanager.getColumns(); + refreshHeader() { + const columns = this.datamanager.getColumns(); - if (!$('.data-table-col', this.header)) { - // insert html + 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 }); - } + let html = this.rowmanager.getRowHTML(columns, { + isHeader: 1 + }); + if (this.options.enableInlineFilters) { + html += this.rowmanager.getRowHTML(columns, { + isFilter: 1 + }); + } - $('thead', this.header).innerHTML = html; + $('thead', this.header).innerHTML = html; - this.$filterRow = $('.data-table-row[data-is-filter]', this.header); + 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; - } + 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 - }); + $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]; + // refresh sort indicator + const sortIndicator = $('.sort-indicator', $col); + if (sortIndicator) { + sortIndicator.innerHTML = this.options.sortIndicator[column.sortOrder]; + } + }); } - }); + // reset columnMap + this.$columnMap = []; } - // 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; + bindEvents() { + this.bindDropdown(); + this.bindResizeColumn(); + this.bindMoveColumn(); + this.bindFilter(); } - } - bindResizeColumn() { - let isDragging = false; - let $resizingCell, startWidth, startX; + bindDropdown() { + let $activeDropdown; + $.on(this.header, 'click', '.data-table-dropdown-toggle', (e, $button) => { + const $dropdown = $.closest('.data-table-dropdown', $button); - $.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' + if (!$dropdown.classList.contains('is-active')) { + deactivateDropdown(); + $dropdown.classList.add('is-active'); + $activeDropdown = $dropdown; + } else { + deactivateDropdown(); + } }); - }); - 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(document.body, 'click', (e) => { + if (e.target.matches('.data-table-dropdown-toggle')) return; + deactivateDropdown(); }); - }; - $.on(this.header, 'keydown', '.data-table-filter', debounce$2(handler, 300)); - } - sortRows(colIndex, sortOrder) { - return this.datamanager.sortRows(colIndex, sortOrder); - } + const dropdownItems = this.options.headerDropdown; - getColumn(colIndex) { - return this.datamanager.getColumn(colIndex); - } + $.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; - getColumns() { - return this.datamanager.getColumns(); - } + callback && callback.call(this.instance, this.getColumn(colIndex)); + }); - 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; + function deactivateDropdown(e) { + $activeDropdown && $activeDropdown.classList.remove('is-active'); + $activeDropdown = null; + } } - $column.style.width = width + 'px'; - } + bindResizeColumn() { + let isDragging = false; + let $resizingCell, startWidth, startX; - getColumnMinWidth(colIndex) { - colIndex = +colIndex; - return this.getColumn(colIndex).minWidth || 24; - } + $.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); - getFirstColumnIndex() { - if (this.options.addCheckboxColumn && this.options.addSerialNoColumn) { - return 2; + 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); + }); } - if (this.options.addCheckboxColumn || this.options.addSerialNoColumn) { - return 1; + 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); } - return 0; - } + bindSortColumn() { - getHeaderCell$(colIndex) { - return $(`.data-table-col[data-col-index="${colIndex}"]`, this.header); - } + $.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); - getLastColumnIndex() { - return this.datamanager.getColumnCount() - 1; - } + if (col && col.sortable === false) { + return; + } - getSerialColumnIndex() { - const columns = this.datamanager.getColumns(); + // reset sort indicator + $('.sort-indicator', this.header).textContent = ''; + $.each('.data-table-col', this.header).map($cell => { + $.data($cell, { + sortOrder: 'none' + }); + }); - return columns.findIndex(column => column.content.includes('Sr. No')); - } + 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; + // add dropdown buttons + const dropdownItems = this.options.headerDropdown; - return `
${dropdownButton}
+ return `
${dropdownButton}
${dropdownItems.map((d, i) => `
${d.label}
`).join('')}
@@ -1727,603 +1787,649 @@ var getDropdownHTML = function getDropdownHTML(dropdownButton = 'v') { }; class CellManager { - constructor(instance) { - this.instance = instance; - this.wrapper = this.instance.wrapper; - this.options = this.instance.options; - this.style = this.instance.style; - this.bodyScrollable = this.instance.bodyScrollable; - this.columnmanager = this.instance.columnmanager; - this.rowmanager = this.instance.rowmanager; - this.datamanager = this.instance.datamanager; - this.keyboard = this.instance.keyboard; + constructor(instance) { + this.instance = instance; + this.wrapper = this.instance.wrapper; + this.options = this.instance.options; + this.style = this.instance.style; + this.bodyScrollable = this.instance.bodyScrollable; + this.columnmanager = this.instance.columnmanager; + this.rowmanager = this.instance.rowmanager; + this.datamanager = this.instance.datamanager; + this.keyboard = this.instance.keyboard; - this.bindEvents(); - } + this.bindEvents(); + } - bindEvents() { - this.bindFocusCell(); - this.bindEditCell(); - this.bindKeyboardSelection(); - this.bindCopyCellContents(); - this.bindMouseEvents(); - } + bindEvents() { + this.bindFocusCell(); + this.bindEditCell(); + this.bindKeyboardSelection(); + this.bindCopyCellContents(); + this.bindMouseEvents(); + } - bindFocusCell() { - this.bindKeyboardNav(); - } + bindFocusCell() { + this.bindKeyboardNav(); + } - bindEditCell() { - this.$editingCell = null; + bindEditCell() { + this.$editingCell = null; - $.on(this.bodyScrollable, 'dblclick', '.data-table-col', (e, cell) => { - this.activateEditing(cell); - }); + $.on(this.bodyScrollable, 'dblclick', '.data-table-col', (e, cell) => { + this.activateEditing(cell); + }); + + this.keyboard.on('enter', (e) => { + if (this.$focusedCell && !this.$editingCell) { + // enter keypress on focused cell + this.activateEditing(this.$focusedCell); + } else if (this.$editingCell) { + // enter keypress on editing cell + this.submitEditing(); + this.deactivateEditing(); + } + }); + } + + bindKeyboardNav() { + const focusCell = (direction) => { + if (!this.$focusedCell || this.$editingCell) { + return false; + } + + let $cell = this.$focusedCell; + + if (direction === 'left' || direction === 'shift+tab') { + $cell = this.getLeftCell$($cell); + } else if (direction === 'right' || direction === 'tab') { + $cell = this.getRightCell$($cell); + } else if (direction === 'up') { + $cell = this.getAboveCell$($cell); + } else if (direction === 'down') { + $cell = this.getBelowCell$($cell); + } + + this.focusCell($cell); + return true; + }; + + const focusLastCell = (direction) => { + if (!this.$focusedCell || this.$editingCell) { + return false; + } + + let $cell = this.$focusedCell; + const { + rowIndex, + colIndex + } = $.data($cell); + + if (direction === 'left') { + $cell = this.getLeftMostCell$(rowIndex); + } else if (direction === 'right') { + $cell = this.getRightMostCell$(rowIndex); + } else if (direction === 'up') { + $cell = this.getTopMostCell$(colIndex); + } else if (direction === 'down') { + $cell = this.getBottomMostCell$(colIndex); + } + + this.focusCell($cell); + return true; + }; + + ['left', 'right', 'up', 'down', 'tab', 'shift+tab'].map( + direction => this.keyboard.on(direction, () => focusCell(direction)) + ); + + ['left', 'right', 'up', 'down'].map( + direction => this.keyboard.on('ctrl+' + direction, () => focusLastCell(direction)) + ); + + this.keyboard.on('esc', () => { + this.deactivateEditing(); + }); + + if (this.options.enableInlineFilters) { + this.keyboard.on('ctrl+f', (e) => { + const $cell = $.closest('.data-table-col', e.target); + let { + colIndex + } = $.data($cell); + + this.activateFilter(colIndex); + return true; + }); + } + } + + bindKeyboardSelection() { + const getNextSelectionCursor = (direction) => { + let $selectionCursor = this.getSelectionCursor(); + + if (direction === 'left') { + $selectionCursor = this.getLeftCell$($selectionCursor); + } else if (direction === 'right') { + $selectionCursor = this.getRightCell$($selectionCursor); + } else if (direction === 'up') { + $selectionCursor = this.getAboveCell$($selectionCursor); + } else if (direction === 'down') { + $selectionCursor = this.getBelowCell$($selectionCursor); + } + + return $selectionCursor; + }; + + ['left', 'right', 'up', 'down'].map( + direction => this.keyboard.on('shift+' + direction, + () => this.selectArea(getNextSelectionCursor(direction))) + ); + } + + bindCopyCellContents() { + this.keyboard.on('ctrl+c', () => { + this.copyCellContents(this.$focusedCell, this.$selectionCursor); + }); + } + + bindMouseEvents() { + let mouseDown = null; + + $.on(this.bodyScrollable, 'mousedown', '.data-table-col', (e) => { + mouseDown = true; + this.focusCell($(e.delegatedTarget)); + }); + + $.on(this.bodyScrollable, 'mouseup', () => { + mouseDown = false; + }); + + const selectArea = (e) => { + if (!mouseDown) return; + this.selectArea($(e.delegatedTarget)); + }; + + $.on(this.bodyScrollable, 'mousemove', '.data-table-col', throttle$1(selectArea, 50)); + } + + focusCell($cell, { + skipClearSelection = 0 + } = {}) { + if (!$cell) return; + + // don't focus if already editing cell + if ($cell === this.$editingCell) return; + + const { + colIndex, + isHeader + } = $.data($cell); + if (isHeader) { + return; + } + + const column = this.columnmanager.getColumn(colIndex); + if (column.focusable === false) { + return; + } + + this.scrollToCell($cell); - this.keyboard.on('enter', (e) => { - if (this.$focusedCell && !this.$editingCell) { - // enter keypress on focused cell - this.activateEditing(this.$focusedCell); - } else if (this.$editingCell) { - // enter keypress on editing cell - this.submitEditing(); this.deactivateEditing(); - } - }); - } + if (!skipClearSelection) { + this.clearSelection(); + } - bindKeyboardNav() { - const focusCell = (direction) => { - if (!this.$focusedCell || this.$editingCell) { - return false; - } + if (this.$focusedCell) { + this.$focusedCell.classList.remove('selected'); + } - let $cell = this.$focusedCell; + this.$focusedCell = $cell; + $cell.classList.add('selected'); - if (direction === 'left' || direction === 'shift+tab') { - $cell = this.getLeftCell$($cell); - } else if (direction === 'right' || direction === 'tab') { - $cell = this.getRightCell$($cell); - } else if (direction === 'up') { - $cell = this.getAboveCell$($cell); - } else if (direction === 'down') { - $cell = this.getBelowCell$($cell); - } - - this.focusCell($cell); - return true; - }; - - const focusLastCell = (direction) => { - if (!this.$focusedCell || this.$editingCell) { - return false; - } - - let $cell = this.$focusedCell; - const { rowIndex, colIndex } = $.data($cell); - - if (direction === 'left') { - $cell = this.getLeftMostCell$(rowIndex); - } else if (direction === 'right') { - $cell = this.getRightMostCell$(rowIndex); - } else if (direction === 'up') { - $cell = this.getTopMostCell$(colIndex); - } else if (direction === 'down') { - $cell = this.getBottomMostCell$(colIndex); - } - - this.focusCell($cell); - return true; - }; - - ['left', 'right', 'up', 'down', 'tab', 'shift+tab'].map( - direction => this.keyboard.on(direction, () => focusCell(direction)) - ); - - ['left', 'right', 'up', 'down'].map( - direction => this.keyboard.on('ctrl+' + direction, () => focusLastCell(direction)) - ); - - this.keyboard.on('esc', () => { - this.deactivateEditing(); - }); - - if (this.options.enableInlineFilters) { - this.keyboard.on('ctrl+f', (e) => { - const $cell = $.closest('.data-table-col', e.target); - let { colIndex } = $.data($cell); - - this.activateFilter(colIndex); - return true; - }); - } - } - - bindKeyboardSelection() { - const getNextSelectionCursor = (direction) => { - let $selectionCursor = this.getSelectionCursor(); - - if (direction === 'left') { - $selectionCursor = this.getLeftCell$($selectionCursor); - } else if (direction === 'right') { - $selectionCursor = this.getRightCell$($selectionCursor); - } else if (direction === 'up') { - $selectionCursor = this.getAboveCell$($selectionCursor); - } else if (direction === 'down') { - $selectionCursor = this.getBelowCell$($selectionCursor); - } - - return $selectionCursor; - }; - - ['left', 'right', 'up', 'down'].map( - direction => this.keyboard.on('shift+' + direction, - () => this.selectArea(getNextSelectionCursor(direction))) - ); - } - - bindCopyCellContents() { - this.keyboard.on('ctrl+c', () => { - this.copyCellContents(this.$focusedCell, this.$selectionCursor); - }); - } - - bindMouseEvents() { - let mouseDown = null; - - $.on(this.bodyScrollable, 'mousedown', '.data-table-col', (e) => { - mouseDown = true; - this.focusCell($(e.delegatedTarget)); - }); - - $.on(this.bodyScrollable, 'mouseup', () => { - mouseDown = false; - }); - - const selectArea = (e) => { - if (!mouseDown) return; - this.selectArea($(e.delegatedTarget)); - }; - - $.on(this.bodyScrollable, 'mousemove', '.data-table-col', throttle$1(selectArea, 50)); - } - - focusCell($cell, { skipClearSelection = 0 } = {}) { - if (!$cell) return; - - // don't focus if already editing cell - if ($cell === this.$editingCell) return; - - const { colIndex, isHeader } = $.data($cell); - if (isHeader) { - return; - } - - const column = this.columnmanager.getColumn(colIndex); - if (column.focusable === false) { - return; - } - - this.scrollToCell($cell); - - this.deactivateEditing(); - if (!skipClearSelection) { - this.clearSelection(); - } - - if (this.$focusedCell) { - this.$focusedCell.classList.remove('selected'); - } - - this.$focusedCell = $cell; - $cell.classList.add('selected'); - - // so that keyboard nav works - $cell.focus(); - - this.highlightRowColumnHeader($cell); - } - - highlightRowColumnHeader($cell) { - const { colIndex, rowIndex } = $.data($cell); - const _colIndex = this.datamanager.getColumnIndexById('_rowIndex'); - const colHeaderSelector = `.data-table-header .data-table-col[data-col-index="${colIndex}"]`; - const rowHeaderSelector = `.data-table-col[data-row-index="${rowIndex}"][data-col-index="${_colIndex}"]`; - - if (this.lastHeaders) { - $.removeStyle(this.lastHeaders, 'backgroundColor'); - } - - const colHeader = $(colHeaderSelector, this.wrapper); - const rowHeader = $(rowHeaderSelector, this.wrapper); - - $.style([colHeader, rowHeader], { - backgroundColor: '#f5f7fa' // light-bg - }); - - this.lastHeaders = [colHeader, rowHeader]; - } - - selectAreaOnClusterChanged() { - if (!(this.$focusedCell && this.$selectionCursor)) return; - const { colIndex, rowIndex } = $.data(this.$selectionCursor); - const $cell = this.getCell$(colIndex, rowIndex); - - if (!$cell || $cell === this.$selectionCursor) return; - - // selectArea needs $focusedCell - const fCell = $.data(this.$focusedCell); - this.$focusedCell = this.getCell$(fCell.colIndex, fCell.rowIndex); - - this.selectArea($cell); - } - - focusCellOnClusterChanged() { - if (!this.$focusedCell) return; - - const { colIndex, rowIndex } = $.data(this.$focusedCell); - const $cell = this.getCell$(colIndex, rowIndex); - - if (!$cell) return; - // this function is called after selectAreaOnClusterChanged, - // focusCell calls clearSelection which resets the area selection - // so a flag to skip it - this.focusCell($cell, { skipClearSelection: 1 }); - } - - selectArea($selectionCursor) { - if (!this.$focusedCell) return; - - if (this._selectArea(this.$focusedCell, $selectionCursor)) { - // valid selection - this.$selectionCursor = $selectionCursor; - } - }; - - _selectArea($cell1, $cell2) { - if ($cell1 === $cell2) return false; - - const cells = this.getCellsInRange($cell1, $cell2); - if (!cells) return false; - - this.clearSelection(); - cells.map(index => this.getCell$(...index)).map($cell => $cell.classList.add('highlight')); - return true; - } - - getCellsInRange($cell1, $cell2) { - let colIndex1, rowIndex1, colIndex2, rowIndex2; - - if (typeof $cell1 === 'number') { - [colIndex1, rowIndex1, colIndex2, rowIndex2] = arguments; - } else - if (typeof $cell1 === 'object') { - - if (!($cell1 && $cell2)) { - return false; - } - - const cell1 = $.data($cell1); - const cell2 = $.data($cell2); - - colIndex1 = cell1.colIndex; - rowIndex1 = cell1.rowIndex; - colIndex2 = cell2.colIndex; - rowIndex2 = cell2.rowIndex; - } - - if (rowIndex1 > rowIndex2) { - [rowIndex1, rowIndex2] = [rowIndex2, rowIndex1]; - } - - if (colIndex1 > colIndex2) { - [colIndex1, colIndex2] = [colIndex2, colIndex1]; - } - - if (this.isStandardCell(colIndex1) || this.isStandardCell(colIndex2)) { - return false; - } - - let cells = []; - let colIndex = colIndex1; - let rowIndex = rowIndex1; - let rowIndices = []; - - while (rowIndex <= rowIndex2) { - rowIndices.push(rowIndex); - rowIndex++; - } - - rowIndices.map(rowIndex => { - while (colIndex <= colIndex2) { - cells.push([colIndex, rowIndex]); - colIndex++; - } - colIndex = colIndex1; - }); - - return cells; - } - - clearSelection() { - $.each('.data-table-col.highlight', this.bodyScrollable) - .map(cell => cell.classList.remove('highlight')); - - this.$selectionCursor = null; - } - - getSelectionCursor() { - return this.$selectionCursor || this.$focusedCell; - } - - activateEditing($cell) { - this.focusCell($cell); - const { rowIndex, colIndex } = $.data($cell); - - const col = this.columnmanager.getColumn(colIndex); - if (col && (col.editable === false || col.focusable === false)) { - return; - } - - const cell = this.getCell(colIndex, rowIndex); - if (cell && cell.editable === false) { - return; - } - - if (this.$editingCell) { - const { _rowIndex, _colIndex } = $.data(this.$editingCell); - - if (rowIndex === _rowIndex && colIndex === _colIndex) { - // editing the same cell - return; - } - } - - this.$editingCell = $cell; - $cell.classList.add('editing'); - - const $editCell = $('.edit-cell', $cell); - $editCell.innerHTML = ''; - - const editor = this.getEditor(colIndex, rowIndex, cell.content, $editCell); - - if (editor) { - this.currentCellEditor = editor; - // initialize editing input with cell value - editor.initValue(cell.content, rowIndex, col); - } - } - - deactivateEditing() { - // keep focus on the cell so that keyboard navigation works - if (this.$focusedCell) this.$focusedCell.focus(); - - if (!this.$editingCell) return; - this.$editingCell.classList.remove('editing'); - this.$editingCell = null; - } - - getEditor(colIndex, rowIndex, value, parent) { - // debugger; - const obj = this.options.getEditor(colIndex, rowIndex, value, parent); - if (obj && obj.setValue) return obj; - - // editing fallback - const $input = $.create('input', { - class: 'input-style', - type: 'text', - inside: parent - }); - - return { - initValue(value) { - $input.focus(); - $input.value = value; - }, - getValue() { - return $input.value; - }, - setValue(value) { - $input.value = value; - } - }; - } - - submitEditing() { - if (!this.$editingCell) return; - const $cell = this.$editingCell; - const { rowIndex, colIndex } = $.data($cell); - const col = this.datamanager.getColumn(colIndex); - - if ($cell) { - const editor = this.currentCellEditor; - - if (editor) { - const value = editor.getValue(); - const done = editor.setValue(value, rowIndex, col); - const oldValue = this.getCell(colIndex, rowIndex).content; - - // update cell immediately - this.updateCell(colIndex, rowIndex, value); + // so that keyboard nav works $cell.focus(); - if (done && done.then) { - // revert to oldValue if promise fails - done.catch((e) => { - console.log(e); - this.updateCell(colIndex, rowIndex, oldValue); - }); + this.highlightRowColumnHeader($cell); + } + + highlightRowColumnHeader($cell) { + const { + colIndex, + rowIndex + } = $.data($cell); + const _colIndex = this.datamanager.getColumnIndexById('_rowIndex'); + const colHeaderSelector = `.data-table-header .data-table-col[data-col-index="${colIndex}"]`; + const rowHeaderSelector = `.data-table-col[data-row-index="${rowIndex}"][data-col-index="${_colIndex}"]`; + + if (this.lastHeaders) { + $.removeStyle(this.lastHeaders, 'backgroundColor'); } - } + + const colHeader = $(colHeaderSelector, this.wrapper); + const rowHeader = $(rowHeaderSelector, this.wrapper); + + $.style([colHeader, rowHeader], { + backgroundColor: '#f5f7fa' // light-bg + }); + + this.lastHeaders = [colHeader, rowHeader]; } - this.currentCellEditor = null; - } + selectAreaOnClusterChanged() { + if (!(this.$focusedCell && this.$selectionCursor)) return; + const { + colIndex, + rowIndex + } = $.data(this.$selectionCursor); + const $cell = this.getCell$(colIndex, rowIndex); - copyCellContents($cell1, $cell2) { - if (!$cell2 && $cell1) { - // copy only focusedCell - const { colIndex, rowIndex } = $.data($cell1); - const cell = this.getCell(colIndex, rowIndex); - copyTextToClipboard(cell.content); - return; + if (!$cell || $cell === this.$selectionCursor) return; + + // selectArea needs $focusedCell + const fCell = $.data(this.$focusedCell); + this.$focusedCell = this.getCell$(fCell.colIndex, fCell.rowIndex); + + this.selectArea($cell); } - const cells = this.getCellsInRange($cell1, $cell2); - if (!cells) return; + focusCellOnClusterChanged() { + if (!this.$focusedCell) return; - const values = cells - // get cell objects - .map(index => this.getCell(...index)) - // convert to array of rows - .reduce((acc, curr) => { - const rowIndex = curr.rowIndex; + const { + colIndex, + rowIndex + } = $.data(this.$focusedCell); + const $cell = this.getCell$(colIndex, rowIndex); - acc[rowIndex] = acc[rowIndex] || []; - acc[rowIndex].push(curr.content); - - return acc; - }, []) - // join values by tab - .map(row => row.join('\t')) - // join rows by newline - .join('\n'); - - copyTextToClipboard(values); - } - - activateFilter(colIndex) { - this.columnmanager.toggleFilter(); - this.columnmanager.focusFilter(colIndex); - - if (!this.columnmanager.isFilterShown) { - // put focus back on cell - this.$focusedCell.focus(); + if (!$cell) return; + // this function is called after selectAreaOnClusterChanged, + // focusCell calls clearSelection which resets the area selection + // so a flag to skip it + this.focusCell($cell, { + skipClearSelection: 1 + }); } - } - updateCell(colIndex, rowIndex, value) { - const cell = this.datamanager.updateCell(colIndex, rowIndex, { - content: value - }); - this.refreshCell(cell); - } + selectArea($selectionCursor) { + if (!this.$focusedCell) return; - refreshCell(cell) { - const $cell = $(this.cellSelector(cell.colIndex, cell.rowIndex), this.bodyScrollable); - $cell.innerHTML = this.getCellContent(cell); - } + if (this._selectArea(this.$focusedCell, $selectionCursor)) { + // valid selection + this.$selectionCursor = $selectionCursor; + } + }; - isStandardCell(colIndex) { - // Standard cells are in Sr. No and Checkbox column - return colIndex < this.columnmanager.getFirstColumnIndex(); - } + _selectArea($cell1, $cell2) { + if ($cell1 === $cell2) return false; - getCell$(colIndex, rowIndex) { - return $(this.cellSelector(colIndex, rowIndex), this.bodyScrollable); - } + const cells = this.getCellsInRange($cell1, $cell2); + if (!cells) return false; - getAboveCell$($cell) { - const { colIndex } = $.data($cell); - const $aboveRow = $cell.parentElement.previousElementSibling; + this.clearSelection(); + cells.map(index => this.getCell$(...index)).map($cell => $cell.classList.add('highlight')); + return true; + } - return $(`[data-col-index="${colIndex}"]`, $aboveRow); - } + getCellsInRange($cell1, $cell2) { + let colIndex1, rowIndex1, colIndex2, rowIndex2; - getBelowCell$($cell) { - const { colIndex } = $.data($cell); - const $belowRow = $cell.parentElement.nextElementSibling; + if (typeof $cell1 === 'number') { + [colIndex1, rowIndex1, colIndex2, rowIndex2] = arguments; + } else + if (typeof $cell1 === 'object') { - return $(`[data-col-index="${colIndex}"]`, $belowRow); - } + if (!($cell1 && $cell2)) { + return false; + } - getLeftCell$($cell) { - return $cell.previousElementSibling; - } + const cell1 = $.data($cell1); + const cell2 = $.data($cell2); - getRightCell$($cell) { - return $cell.nextElementSibling; - } + colIndex1 = cell1.colIndex; + rowIndex1 = cell1.rowIndex; + colIndex2 = cell2.colIndex; + rowIndex2 = cell2.rowIndex; + } - getLeftMostCell$(rowIndex) { - return this.getCell$(this.columnmanager.getFirstColumnIndex(), rowIndex); - } + if (rowIndex1 > rowIndex2) { + [rowIndex1, rowIndex2] = [rowIndex2, rowIndex1]; + } - getRightMostCell$(rowIndex) { - return this.getCell$(this.columnmanager.getLastColumnIndex(), rowIndex); - } + if (colIndex1 > colIndex2) { + [colIndex1, colIndex2] = [colIndex2, colIndex1]; + } - getTopMostCell$(colIndex) { - return this.getCell$(colIndex, this.rowmanager.getFirstRowIndex()); - } + if (this.isStandardCell(colIndex1) || this.isStandardCell(colIndex2)) { + return false; + } - getBottomMostCell$(colIndex) { - return this.getCell$(colIndex, this.rowmanager.getLastRowIndex()); - } + let cells = []; + let colIndex = colIndex1; + let rowIndex = rowIndex1; + let rowIndices = []; - getCell(colIndex, rowIndex) { - return this.instance.datamanager.getCell(colIndex, rowIndex); - } + while (rowIndex <= rowIndex2) { + rowIndices.push(rowIndex); + rowIndex++; + } - getCellAttr($cell) { - return this.instance.getCellAttr($cell); - } + rowIndices.map(rowIndex => { + while (colIndex <= colIndex2) { + cells.push([colIndex, rowIndex]); + colIndex++; + } + colIndex = colIndex1; + }); - getRowHeight() { - return $.style($('.data-table-row', this.bodyScrollable), 'height'); - } + return cells; + } - scrollToCell($cell) { - if ($.inViewport($cell, this.bodyScrollable)) return false; + clearSelection() { + $.each('.data-table-col.highlight', this.bodyScrollable) + .map(cell => cell.classList.remove('highlight')); - const { rowIndex } = $.data($cell); - this.rowmanager.scrollToRow(rowIndex); - return false; - } + this.$selectionCursor = null; + } - getRowCountPerPage() { - return Math.ceil(this.instance.getViewportHeight() / this.getRowHeight()); - } + getSelectionCursor() { + return this.$selectionCursor || this.$focusedCell; + } - getCellHTML(cell) { - const { rowIndex, colIndex, isHeader, isFilter } = cell; - const dataAttr = makeDataAttributeString({ - rowIndex, - colIndex, - isHeader, - isFilter - }); + activateEditing($cell) { + this.focusCell($cell); + const { + rowIndex, + colIndex + } = $.data($cell); - return ` + const col = this.columnmanager.getColumn(colIndex); + if (col && (col.editable === false || col.focusable === false)) { + return; + } + + const cell = this.getCell(colIndex, rowIndex); + if (cell && cell.editable === false) { + return; + } + + if (this.$editingCell) { + const { + _rowIndex, + _colIndex + } = $.data(this.$editingCell); + + if (rowIndex === _rowIndex && colIndex === _colIndex) { + // editing the same cell + return; + } + } + + this.$editingCell = $cell; + $cell.classList.add('editing'); + + const $editCell = $('.edit-cell', $cell); + $editCell.innerHTML = ''; + + const editor = this.getEditor(colIndex, rowIndex, cell.content, $editCell); + + if (editor) { + this.currentCellEditor = editor; + // initialize editing input with cell value + editor.initValue(cell.content, rowIndex, col); + } + } + + deactivateEditing() { + // keep focus on the cell so that keyboard navigation works + if (this.$focusedCell) this.$focusedCell.focus(); + + if (!this.$editingCell) return; + this.$editingCell.classList.remove('editing'); + this.$editingCell = null; + } + + getEditor(colIndex, rowIndex, value, parent) { + // debugger; + const obj = this.options.getEditor(colIndex, rowIndex, value, parent); + if (obj && obj.setValue) return obj; + + // editing fallback + const $input = $.create('input', { + class: 'input-style', + type: 'text', + inside: parent + }); + + return { + initValue(value) { + $input.focus(); + $input.value = value; + }, + getValue() { + return $input.value; + }, + setValue(value) { + $input.value = value; + } + }; + } + + submitEditing() { + if (!this.$editingCell) return; + const $cell = this.$editingCell; + const { + rowIndex, + colIndex + } = $.data($cell); + const col = this.datamanager.getColumn(colIndex); + + if ($cell) { + const editor = this.currentCellEditor; + + if (editor) { + const value = editor.getValue(); + const done = editor.setValue(value, rowIndex, col); + const oldValue = this.getCell(colIndex, rowIndex).content; + + // update cell immediately + this.updateCell(colIndex, rowIndex, value); + $cell.focus(); + + if (done && done.then) { + // revert to oldValue if promise fails + done.catch((e) => { + console.log(e); + this.updateCell(colIndex, rowIndex, oldValue); + }); + } + } + } + + this.currentCellEditor = null; + } + + copyCellContents($cell1, $cell2) { + if (!$cell2 && $cell1) { + // copy only focusedCell + const { + colIndex, + rowIndex + } = $.data($cell1); + const cell = this.getCell(colIndex, rowIndex); + copyTextToClipboard(cell.content); + return; + } + const cells = this.getCellsInRange($cell1, $cell2); + + if (!cells) return; + + const values = cells + // get cell objects + .map(index => this.getCell(...index)) + // convert to array of rows + .reduce((acc, curr) => { + const rowIndex = curr.rowIndex; + + acc[rowIndex] = acc[rowIndex] || []; + acc[rowIndex].push(curr.content); + + return acc; + }, []) + // join values by tab + .map(row => row.join('\t')) + // join rows by newline + .join('\n'); + + copyTextToClipboard(values); + } + + activateFilter(colIndex) { + this.columnmanager.toggleFilter(); + this.columnmanager.focusFilter(colIndex); + + if (!this.columnmanager.isFilterShown) { + // put focus back on cell + this.$focusedCell.focus(); + } + } + + updateCell(colIndex, rowIndex, value) { + const cell = this.datamanager.updateCell(colIndex, rowIndex, { + content: value + }); + this.refreshCell(cell); + } + + refreshCell(cell) { + const $cell = $(this.cellSelector(cell.colIndex, cell.rowIndex), this.bodyScrollable); + $cell.innerHTML = this.getCellContent(cell); + } + + isStandardCell(colIndex) { + // Standard cells are in Sr. No and Checkbox column + return colIndex < this.columnmanager.getFirstColumnIndex(); + } + + getCell$(colIndex, rowIndex) { + return $(this.cellSelector(colIndex, rowIndex), this.bodyScrollable); + } + + getAboveCell$($cell) { + const { + colIndex + } = $.data($cell); + const $aboveRow = $cell.parentElement.previousElementSibling; + + return $(`[data-col-index="${colIndex}"]`, $aboveRow); + } + + getBelowCell$($cell) { + const { + colIndex + } = $.data($cell); + const $belowRow = $cell.parentElement.nextElementSibling; + + return $(`[data-col-index="${colIndex}"]`, $belowRow); + } + + getLeftCell$($cell) { + return $cell.previousElementSibling; + } + + getRightCell$($cell) { + return $cell.nextElementSibling; + } + + getLeftMostCell$(rowIndex) { + return this.getCell$(this.columnmanager.getFirstColumnIndex(), rowIndex); + } + + getRightMostCell$(rowIndex) { + return this.getCell$(this.columnmanager.getLastColumnIndex(), rowIndex); + } + + getTopMostCell$(colIndex) { + return this.getCell$(colIndex, this.rowmanager.getFirstRowIndex()); + } + + getBottomMostCell$(colIndex) { + return this.getCell$(colIndex, this.rowmanager.getLastRowIndex()); + } + + getCell(colIndex, rowIndex) { + return this.instance.datamanager.getCell(colIndex, rowIndex); + } + + getCellAttr($cell) { + return this.instance.getCellAttr($cell); + } + + getRowHeight() { + return $.style($('.data-table-row', this.bodyScrollable), 'height'); + } + + scrollToCell($cell) { + if ($.inViewport($cell, this.bodyScrollable)) return false; + + const { + rowIndex + } = $.data($cell); + this.rowmanager.scrollToRow(rowIndex); + return false; + } + + getRowCountPerPage() { + return Math.ceil(this.instance.getViewportHeight() / this.getRowHeight()); + } + + getCellHTML(cell) { + const { + rowIndex, + colIndex, + isHeader, + isFilter + } = cell; + const dataAttr = makeDataAttributeString({ + rowIndex, + colIndex, + isHeader, + isFilter + }); + + return ` ${this.getCellContent(cell)} `; - } - - getCellContent(cell) { - const { isHeader } = cell; - - const editable = !isHeader && cell.editable !== false; - const editCellHTML = editable ? this.getEditCellHTML() : ''; - - const sortable = isHeader && cell.sortable !== false; - const sortIndicator = sortable ? '' : ''; - - const resizable = isHeader && cell.resizable !== false; - const resizeColumn = resizable ? '' : ''; - - const hasDropdown = isHeader && cell.dropdown !== false; - const dropdown = hasDropdown ? `
${getDropdownHTML()}
` : ''; - - let contentHTML; - if (cell.isHeader || cell.isFilter || !cell.column.format) { - contentHTML = cell.content; - } else { - contentHTML = cell.column.format(cell.content, cell); } - return ` + getCellContent(cell) { + const { + isHeader + } = cell; + + const editable = !isHeader && cell.editable !== false; + const editCellHTML = editable ? this.getEditCellHTML() : ''; + + const sortable = isHeader && cell.sortable !== false; + const sortIndicator = sortable ? '' : ''; + + const resizable = isHeader && cell.resizable !== false; + const resizeColumn = resizable ? '' : ''; + + const hasDropdown = isHeader && cell.dropdown !== false; + const dropdown = hasDropdown ? `
${getDropdownHTML()}
` : ''; + + let contentHTML; + if (cell.isHeader || cell.isFilter || !cell.column.format) { + contentHTML = cell.content; + } else { + contentHTML = cell.column.format(cell.content, cell); + } + + return `
${(contentHTML)} ${sortIndicator} @@ -2332,321 +2438,332 @@ class CellManager {
${editCellHTML} `; - } + } - getEditCellHTML() { - return ` + getEditCellHTML() { + return `
`; - } + } - cellSelector(colIndex, rowIndex) { - return `.data-table-col[data-col-index="${colIndex}"][data-row-index="${rowIndex}"]`; - } + cellSelector(colIndex, rowIndex) { + return `.data-table-col[data-col-index="${colIndex}"][data-row-index="${rowIndex}"]`; + } } class RowManager { - constructor(instance) { - this.instance = instance; - this.options = this.instance.options; - this.wrapper = this.instance.wrapper; - this.bodyScrollable = this.instance.bodyScrollable; + constructor(instance) { + this.instance = instance; + this.options = this.instance.options; + this.wrapper = this.instance.wrapper; + this.bodyScrollable = this.instance.bodyScrollable; - this.bindEvents(); - this.refreshRows = promisify(this.refreshRows, this); - } - - get datamanager() { - return this.instance.datamanager; - } - - get cellmanager() { - return this.instance.cellmanager; - } - - bindEvents() { - this.bindCheckbox(); - } - - bindCheckbox() { - if (!this.options.addCheckboxColumn) return; - - // map of checked rows - this.checkMap = []; - - $.on(this.wrapper, 'click', '.data-table-col[data-col-index="0"] [type="checkbox"]', (e, $checkbox) => { - const $cell = $checkbox.closest('.data-table-col'); - const { rowIndex, isHeader } = $.data($cell); - const checked = $checkbox.checked; - - if (isHeader) { - this.checkAll(checked); - } else { - this.checkRow(rowIndex, checked); - } - }); - } - - refreshRows() { - this.instance.renderBody(); - this.instance.setDimensions(); - } - - refreshRow(row, rowIndex) { - const _row = this.datamanager.updateRow(row, rowIndex); - - _row.forEach(cell => { - this.cellmanager.refreshCell(cell); - }); - } - - getCheckedRows() { - if (!this.checkMap) { - return []; + this.bindEvents(); + this.refreshRows = promisify(this.refreshRows, this); } - return this.checkMap - .map((c, rowIndex) => { - if (c) { - return rowIndex; + get datamanager() { + return this.instance.datamanager; + } + + get cellmanager() { + return this.instance.cellmanager; + } + + bindEvents() { + this.bindCheckbox(); + } + + bindCheckbox() { + if (!this.options.addCheckboxColumn) return; + + // map of checked rows + this.checkMap = []; + + $.on(this.wrapper, 'click', '.data-table-col[data-col-index="0"] [type="checkbox"]', (e, $checkbox) => { + const $cell = $checkbox.closest('.data-table-col'); + const { + rowIndex, + isHeader + } = $.data($cell); + const checked = $checkbox.checked; + + if (isHeader) { + this.checkAll(checked); + } else { + this.checkRow(rowIndex, checked); + } + }); + } + + refreshRows() { + this.instance.renderBody(); + this.instance.setDimensions(); + } + + refreshRow(row, rowIndex) { + const _row = this.datamanager.updateRow(row, rowIndex); + + _row.forEach(cell => { + this.cellmanager.refreshCell(cell); + }); + } + + getCheckedRows() { + if (!this.checkMap) { + return []; } - return null; - }) - .filter(c => { - return c !== null || c !== undefined; - }); - } - highlightCheckedRows() { - this.getCheckedRows() - .map(rowIndex => this.checkRow(rowIndex, true)); - } - - checkRow(rowIndex, toggle) { - const value = toggle ? 1 : 0; - - // update internal map - this.checkMap[rowIndex] = value; - // set checkbox value explicitly - $.each(`.data-table-col[data-row-index="${rowIndex}"][data-col-index="0"] [type="checkbox"]`, this.bodyScrollable) - .map(input => { - input.checked = toggle; - }); - // highlight row - this.highlightRow(rowIndex, toggle); - } - - checkAll(toggle) { - const value = toggle ? 1 : 0; - - // update internal map - if (toggle) { - this.checkMap = Array.from(Array(this.getTotalRows())).map(c => value); - } else { - this.checkMap = []; - } - // set checkbox value - $.each('.data-table-col[data-col-index="0"] [type="checkbox"]', this.bodyScrollable) - .map(input => { - input.checked = toggle; - }); - // highlight all - this.highlightAll(toggle); - } - - highlightRow(rowIndex, toggle = true) { - const $row = this.getRow$(rowIndex); - if (!$row) return; - - if (!toggle && this.bodyScrollable.classList.contains('row-highlight-all')) { - $row.classList.add('row-unhighlight'); - return; + return this.checkMap + .map((c, rowIndex) => { + if (c) { + return rowIndex; + } + return null; + }) + .filter(c => { + return c !== null || c !== undefined; + }); } - if (toggle && $row.classList.contains('row-unhighlight')) { - $row.classList.remove('row-unhighlight'); + highlightCheckedRows() { + this.getCheckedRows() + .map(rowIndex => this.checkRow(rowIndex, true)); } - this._highlightedRows = this._highlightedRows || {}; - - if (toggle) { - $row.classList.add('row-highlight'); - this._highlightedRows[rowIndex] = $row; - } else { - $row.classList.remove('row-highlight'); - delete this._highlightedRows[rowIndex]; - } - } - - highlightAll(toggle = true) { - if (toggle) { - this.bodyScrollable.classList.add('row-highlight-all'); - } else { - this.bodyScrollable.classList.remove('row-highlight-all'); - for (const rowIndex in this._highlightedRows) { - const $row = this._highlightedRows[rowIndex]; - $row.classList.remove('row-highlight'); - } - this._highlightedRows = {}; - } - } - - getRow$(rowIndex) { - return $(`.data-table-row[data-row-index="${rowIndex}"]`, this.bodyScrollable); - } - - getTotalRows() { - return this.datamanager.getRowCount(); - } - - getFirstRowIndex() { - return 0; - } - - getLastRowIndex() { - return this.datamanager.getRowCount() - 1; - } - - scrollToRow(rowIndex) { - rowIndex = +rowIndex; - this._lastScrollTo = this._lastScrollTo || 0; - const $row = this.getRow$(rowIndex); - if ($.inViewport($row, this.bodyScrollable)) return; - - const { height } = $row.getBoundingClientRect(); - const { top, bottom } = this.bodyScrollable.getBoundingClientRect(); - const rowsInView = Math.floor((bottom - top) / height); - - let offset = 0; - if (rowIndex > this._lastScrollTo) { - offset = height * ((rowIndex + 1) - rowsInView); - } else { - offset = height * ((rowIndex + 1) - 1); + checkRow(rowIndex, toggle) { + const value = toggle ? 1 : 0; + const selector = rowIndex => + `.data-table-col[data-row-index="${rowIndex}"][data-col-index="0"] [type="checkbox"]`; + // update internal map + this.checkMap[rowIndex] = value; + // set checkbox value explicitly + $.each(selector(rowIndex), this.bodyScrollable) + .map(input => { + input.checked = toggle; + }); + // highlight row + this.highlightRow(rowIndex, toggle); } - this._lastScrollTo = rowIndex; - $.scrollTop(this.bodyScrollable, offset); - } + checkAll(toggle) { + const value = toggle ? 1 : 0; - getRowHTML(row, props) { - const dataAttr = makeDataAttributeString(props); - - if (props.isFilter) { - row = row.map(cell => (Object.assign(cell, { - content: this.getFilterInput({ colIndex: cell.colIndex }), - isFilter: 1, - isHeader: undefined, - editable: false - }))); + // update internal map + if (toggle) { + this.checkMap = Array.from(Array(this.getTotalRows())).map(c => value); + } else { + this.checkMap = []; + } + // set checkbox value + $.each('.data-table-col[data-col-index="0"] [type="checkbox"]', this.bodyScrollable) + .map(input => { + input.checked = toggle; + }); + // highlight all + this.highlightAll(toggle); } - return ` - - ${row.map(cell => this.cellmanager.getCellHTML(cell)).join('')} - - `; - } + highlightRow(rowIndex, toggle = true) { + const $row = this.getRow$(rowIndex); + if (!$row) return; - getFilterInput(props) { - const dataAttr = makeDataAttributeString(props); - return ``; - } + if (!toggle && this.bodyScrollable.classList.contains('row-highlight-all')) { + $row.classList.add('row-unhighlight'); + return; + } + + if (toggle && $row.classList.contains('row-unhighlight')) { + $row.classList.remove('row-unhighlight'); + } + + this._highlightedRows = this._highlightedRows || {}; + + if (toggle) { + $row.classList.add('row-highlight'); + this._highlightedRows[rowIndex] = $row; + } else { + $row.classList.remove('row-highlight'); + delete this._highlightedRows[rowIndex]; + } + } + + highlightAll(toggle = true) { + if (toggle) { + this.bodyScrollable.classList.add('row-highlight-all'); + } else { + this.bodyScrollable.classList.remove('row-highlight-all'); + for (const rowIndex in this._highlightedRows) { + const $row = this._highlightedRows[rowIndex]; + $row.classList.remove('row-highlight'); + } + this._highlightedRows = {}; + } + } + + getRow$(rowIndex) { + return $(`.data-table-row[data-row-index="${rowIndex}"]`, this.bodyScrollable); + } + + getTotalRows() { + return this.datamanager.getRowCount(); + } + + getFirstRowIndex() { + return 0; + } + + getLastRowIndex() { + return this.datamanager.getRowCount() - 1; + } + + scrollToRow(rowIndex) { + rowIndex = +rowIndex; + this._lastScrollTo = this._lastScrollTo || 0; + const $row = this.getRow$(rowIndex); + if ($.inViewport($row, this.bodyScrollable)) return; + + const { + height + } = $row.getBoundingClientRect(); + const { + top, + bottom + } = this.bodyScrollable.getBoundingClientRect(); + const rowsInView = Math.floor((bottom - top) / height); + + let offset = 0; + if (rowIndex > this._lastScrollTo) { + offset = height * ((rowIndex + 1) - rowsInView); + } else { + offset = height * ((rowIndex + 1) - 1); + } + + this._lastScrollTo = rowIndex; + $.scrollTop(this.bodyScrollable, offset); + } + + getRowHTML(row, props) { + const dataAttr = makeDataAttributeString(props); + + if (props.isFilter) { + row = row.map(cell => (Object.assign(cell, { + content: this.getFilterInput({ + colIndex: cell.colIndex + }), + isFilter: 1, + isHeader: undefined, + editable: false + }))); + } + + return ` + + ${row.map(cell => this.cellmanager.getCellHTML(cell)).join('')} + + `; + } + + getFilterInput(props) { + const dataAttr = makeDataAttributeString(props); + return ``; + } } class BodyRenderer { - constructor(instance) { - this.instance = instance; - this.options = instance.options; - this.datamanager = instance.datamanager; - this.rowmanager = instance.rowmanager; - this.cellmanager = instance.cellmanager; - this.bodyScrollable = instance.bodyScrollable; - this.log = instance.log; - this.appendRemainingData = promisify(this.appendRemainingData, this); - } - - render() { - if (this.options.enableClusterize) { - this.renderBodyWithClusterize(); - } else { - this.renderBodyHTML(); - } - } - - renderBodyHTML() { - const rows = this.datamanager.getRows(); - - this.bodyScrollable.innerHTML = ` - - ${getBodyHTML(rows)} -
- `; - this.instance.setDimensions(); - this.restoreState(); - } - - renderBodyWithClusterize() { - // first page - const rows = this.datamanager.getRows(0, 20); - const initialData = this.getDataForClusterize(rows); - - if (!this.clusterize) { - // empty body - this.bodyScrollable.innerHTML = ` - - ${getBodyHTML([])} -
- `; - - // first 20 rows will appended - // rest of them in nextTick - this.clusterize = new Clusterize({ - rows: initialData, - scrollElem: this.bodyScrollable, - contentElem: $('tbody', this.bodyScrollable), - callbacks: { - clusterChanged: () => { - this.restoreState(); - } - }, - /* eslint-disable */ - no_data_text: this.options.noDataMessage, - no_data_class: 'empty-state' - /* eslint-enable */ - }); - - // setDimensions requires atleast 1 row to exist in dom - this.instance.setDimensions(); - } else { - this.clusterize.update(initialData); + constructor(instance) { + this.instance = instance; + this.options = instance.options; + this.datamanager = instance.datamanager; + this.rowmanager = instance.rowmanager; + this.cellmanager = instance.cellmanager; + this.bodyScrollable = instance.bodyScrollable; + this.log = instance.log; + this.appendRemainingData = promisify(this.appendRemainingData, this); } - this.appendRemainingData(); - } + render() { + if (this.options.enableClusterize) { + this.renderBodyWithClusterize(); + } else { + this.renderBodyHTML(); + } + } - restoreState() { - this.rowmanager.highlightCheckedRows(); - this.cellmanager.selectAreaOnClusterChanged(); - this.cellmanager.focusCellOnClusterChanged(); - } + renderBodyHTML() { + const rows = this.datamanager.getRows(); - appendRemainingData() { - const rows = this.datamanager.getRows(20); - const data = this.getDataForClusterize(rows); - this.clusterize.append(data); - } + this.bodyScrollable.innerHTML = ` + + ${this.getBodyHTML(rows)} +
+ `; + this.instance.setDimensions(); + this.restoreState(); + } - getDataForClusterize(rows) { - return rows.map((row) => this.rowmanager.getRowHTML(row, { rowIndex: row[0].rowIndex })); - } -} + renderBodyWithClusterize() { + // first page + const rows = this.datamanager.getRows(0, 20); + const initialData = this.getDataForClusterize(rows); -function getBodyHTML(rows) { - return ` - - ${rows.map(row => this.rowmanager.getRowHTML(row, { rowIndex: row[0].rowIndex })).join('')} - - `; + if (!this.clusterize) { + // empty body + this.bodyScrollable.innerHTML = ` + + ${this.getBodyHTML([])} +
+ `; + + // first 20 rows will appended + // rest of them in nextTick + this.clusterize = new Clusterize({ + rows: initialData, + scrollElem: this.bodyScrollable, + contentElem: $('tbody', this.bodyScrollable), + callbacks: { + clusterChanged: () => { + this.restoreState(); + } + }, + /* eslint-disable */ + no_data_text: this.options.noDataMessage, + no_data_class: 'empty-state' + /* eslint-enable */ + }); + + // setDimensions requires atleast 1 row to exist in dom + this.instance.setDimensions(); + } else { + this.clusterize.update(initialData); + } + + this.appendRemainingData(); + } + + restoreState() { + this.rowmanager.highlightCheckedRows(); + this.cellmanager.selectAreaOnClusterChanged(); + this.cellmanager.focusCellOnClusterChanged(); + } + + appendRemainingData() { + const rows = this.datamanager.getRows(20); + const data = this.getDataForClusterize(rows); + this.clusterize.append(data); + } + + getDataForClusterize(rows) { + return rows.map((row) => this.rowmanager.getRowHTML(row, row.meta)); + } + + getBodyHTML(rows) { + return ` + + ${rows.map(row => this.rowmanager.getRowHTML(row, row.meta)).join('')} + + `; + } } class Style { @@ -2683,7 +2800,7 @@ class Style { this.styleEl.remove(); } - setStyle(rule, styleMap, index = -1) { + setStyle(selector, styleMap, index = -1) { const styles = Object.keys(styleMap) .map(prop => { if (!prop.includes('-')) { @@ -2692,7 +2809,12 @@ class Style { return `${prop}:${styleMap[prop]};`; }) .join(''); - let ruleString = `.${this.scopeClass} ${rule} { ${styles} }`; + let prefixedSelector = selector + .split(',') + .map(r => `.${this.scopeClass} ${r}`) + .join(','); + + let ruleString = `${prefixedSelector} { ${styles} }`; let _index = this.styleSheet.cssRules.length; if (index !== -1) { @@ -2880,157 +3002,155 @@ class Style { } const KEYCODES = { - 13: 'enter', - 91: 'meta', - 16: 'shift', - 17: 'ctrl', - 18: 'alt', - 37: 'left', - 38: 'up', - 39: 'right', - 40: 'down', - 9: 'tab', - 27: 'esc', - 67: 'c', - 70: 'f' + 13: 'enter', + 91: 'meta', + 16: 'shift', + 17: 'ctrl', + 18: 'alt', + 37: 'left', + 38: 'up', + 39: 'right', + 40: 'down', + 9: 'tab', + 27: 'esc', + 67: 'c', + 70: 'f' }; class Keyboard { - constructor(element) { - this.listeners = {}; - $.on(element, 'keydown', this.handler.bind(this)); - } - - handler(e) { - let key = KEYCODES[e.keyCode]; - - if (e.shiftKey && key !== 'shift') { - key = 'shift+' + key; + constructor(element) { + this.listeners = {}; + $.on(element, 'keydown', this.handler.bind(this)); } - if ((e.ctrlKey && key !== 'ctrl') || (e.metaKey && key !== 'meta')) { - key = 'ctrl+' + key; - } + handler(e) { + let key = KEYCODES[e.keyCode]; - const listeners = this.listeners[key]; - - if (listeners && listeners.length > 0) { - for (let listener of listeners) { - const preventBubbling = listener(e); - if (preventBubbling === undefined || preventBubbling === true) { - e.preventDefault(); + if (e.shiftKey && key !== 'shift') { + key = 'shift+' + key; + } + + if ((e.ctrlKey && key !== 'ctrl') || (e.metaKey && key !== 'meta')) { + key = 'ctrl+' + key; + } + + const listeners = this.listeners[key]; + + if (listeners && listeners.length > 0) { + for (let listener of listeners) { + const preventBubbling = listener(e); + if (preventBubbling === undefined || preventBubbling === true) { + e.preventDefault(); + } + } } - } } - } - on(key, listener) { - const keys = key.split(',').map(k => k.trim()); + on(key, listener) { + const keys = key.split(',').map(k => k.trim()); - keys.map(key => { - this.listeners[key] = this.listeners[key] || []; - this.listeners[key].push(listener); - }); - } + keys.map(key => { + this.listeners[key] = this.listeners[key] || []; + this.listeners[key].push(listener); + }); + } } var DEFAULT_OPTIONS = { - columns: [], - data: [], - dropdownButton: '▼', - headerDropdown: [ - { - label: 'Sort Ascending', - action: function (column) { - this.sortColumn(column.colIndex, 'asc'); - } + columns: [], + data: [], + dropdownButton: '▼', + headerDropdown: [ + { + label: 'Sort Ascending', + action: function (column) { + this.sortColumn(column.colIndex, 'asc'); + } + }, + { + label: 'Sort Descending', + action: function (column) { + this.sortColumn(column.colIndex, 'desc'); + } + }, + { + label: 'Reset sorting', + action: function (column) { + this.sortColumn(column.colIndex, 'none'); + } + }, + { + label: 'Remove column', + action: function (column) { + this.removeColumn(column.colIndex); + } + } + ], + events: { + onRemoveColumn(column) {}, + onSwitchColumn(column1, column2) {}, + onSortColumn(column) {} }, - { - label: 'Sort Descending', - action: function (column) { - this.sortColumn(column.colIndex, 'desc'); - } + sortIndicator: { + asc: '↑', + desc: '↓', + none: '' }, - { - label: 'Reset sorting', - action: function (column) { - this.sortColumn(column.colIndex, 'none'); - } - }, - { - label: 'Remove column', - action: function (column) { - this.removeColumn(column.colIndex); - } - } - ], - events: { - onRemoveColumn(column) {}, - onSwitchColumn(column1, column2) {}, - onSortColumn(column) {} - }, - sortIndicator: { - asc: '↑', - desc: '↓', - none: '' - }, - freezeMessage: '', - getEditor: () => {}, - addSerialNoColumn: true, - addCheckboxColumn: false, - enableClusterize: true, - enableLogs: false, - layout: 'fixed', // fixed, fluid - noDataMessage: 'No Data', - cellHeight: null, - enableInlineFilters: false + freezeMessage: '', + getEditor: () => {}, + addSerialNoColumn: true, + addCheckboxColumn: false, + enableClusterize: true, + enableLogs: false, + layout: 'fixed', // fixed, fluid + noDataMessage: 'No Data', + cellHeight: null, + enableInlineFilters: false }; class DataTable { - constructor(wrapper, options) { - DataTable.instances++; + constructor(wrapper, options) { + DataTable.instances++; - if (typeof wrapper === 'string') { - // css selector - wrapper = document.querySelector(wrapper); - } - this.wrapper = wrapper; - if (!(this.wrapper instanceof HTMLElement)) { - throw new Error('Invalid argument given for `wrapper`'); + if (typeof wrapper === 'string') { + // css selector + wrapper = document.querySelector(wrapper); + } + this.wrapper = wrapper; + if (!(this.wrapper instanceof HTMLElement)) { + throw new Error('Invalid argument given for `wrapper`'); + } + + this.options = Object.assign({}, DEFAULT_OPTIONS, options); + this.options.headerDropdown = + DEFAULT_OPTIONS.headerDropdown + .concat(options.headerDropdown || []); + // custom user events + this.events = Object.assign({}, DEFAULT_OPTIONS.events, options.events || {}); + this.fireEvent = this.fireEvent.bind(this); + + this.prepare(); + + this.style = new Style(this); + this.keyboard = new Keyboard(this.wrapper); + this.datamanager = new DataManager(this.options); + this.rowmanager = new RowManager(this); + this.columnmanager = new ColumnManager(this); + this.cellmanager = new CellManager(this); + this.bodyRenderer = new BodyRenderer(this); + + if (this.options.data) { + this.refresh(); + } } - this.options = Object.assign({}, DEFAULT_OPTIONS, options); - this.options.headerDropdown = - DEFAULT_OPTIONS.headerDropdown - .concat(options.headerDropdown || []); - // custom user events - this.events = Object.assign( - {}, DEFAULT_OPTIONS.events, options.events || {} - ); - this.fireEvent = this.fireEvent.bind(this); - - this.prepare(); - - this.style = new Style(this); - this.keyboard = new Keyboard(this.wrapper); - this.datamanager = new DataManager(this.options); - this.rowmanager = new RowManager(this); - this.columnmanager = new ColumnManager(this); - this.cellmanager = new CellManager(this); - this.bodyRenderer = new BodyRenderer(this); - - if (this.options.data) { - this.refresh(); + prepare() { + this.prepareDom(); + this.unfreeze(); } - } - prepare() { - this.prepareDom(); - this.unfreeze(); - } - - prepareDom() { - this.wrapper.innerHTML = ` + prepareDom() { + this.wrapper.innerHTML = `
@@ -3044,110 +3164,110 @@ class DataTable {
`; - this.datatableWrapper = $('.data-table', this.wrapper); - this.header = $('.data-table-header', this.wrapper); - this.bodyScrollable = $('.body-scrollable', this.wrapper); - this.freezeContainer = $('.freeze-container', this.wrapper); - } - - refresh(data) { - this.datamanager.init(data); - this.render(); - this.setDimensions(); - } - - destroy() { - this.wrapper.innerHTML = ''; - this.style.destroy(); - } - - appendRows(rows) { - this.datamanager.appendRows(rows); - this.rowmanager.refreshRows(); - } - - refreshRow(row, rowIndex) { - this.rowmanager.refreshRow(row, rowIndex); - } - - render() { - this.renderHeader(); - this.renderBody(); - } - - renderHeader() { - this.columnmanager.renderHeader(); - } - - renderBody() { - this.bodyRenderer.render(); - } - - setDimensions() { - this.style.setDimensions(); - } - - getColumn(colIndex) { - return this.datamanager.getColumn(colIndex); - } - - getColumns() { - return this.datamanager.getColumns(); - } - - getRows() { - return this.datamanager.getRows(); - } - - getCell(colIndex, rowIndex) { - return this.datamanager.getCell(colIndex, rowIndex); - } - - getColumnHeaderElement(colIndex) { - return this.columnmanager.getColumnHeaderElement(colIndex); - } - - getViewportHeight() { - if (!this.viewportHeight) { - this.viewportHeight = $.style(this.bodyScrollable, 'height'); + this.datatableWrapper = $('.data-table', this.wrapper); + this.header = $('.data-table-header', this.wrapper); + this.bodyScrollable = $('.body-scrollable', this.wrapper); + this.freezeContainer = $('.freeze-container', this.wrapper); } - return this.viewportHeight; - } - - sortColumn(colIndex, sortOrder) { - this.columnmanager.sortColumn(colIndex, sortOrder); - } - - removeColumn(colIndex) { - this.columnmanager.removeColumn(colIndex); - } - - scrollToLastColumn() { - this.datatableWrapper.scrollLeft = 9999; - } - - freeze() { - $.style(this.freezeContainer, { - display: '' - }); - } - - unfreeze() { - $.style(this.freezeContainer, { - display: 'none' - }); - } - - fireEvent(eventName, ...args) { - this.events[eventName].apply(this, args); - } - - log() { - if (this.options.enableLogs) { - console.log.apply(console, arguments); + refresh(data) { + this.datamanager.init(data); + this.render(); + this.setDimensions(); + } + + destroy() { + this.wrapper.innerHTML = ''; + this.style.destroy(); + } + + appendRows(rows) { + this.datamanager.appendRows(rows); + this.rowmanager.refreshRows(); + } + + refreshRow(row, rowIndex) { + this.rowmanager.refreshRow(row, rowIndex); + } + + render() { + this.renderHeader(); + this.renderBody(); + } + + renderHeader() { + this.columnmanager.renderHeader(); + } + + renderBody() { + this.bodyRenderer.render(); + } + + setDimensions() { + this.style.setDimensions(); + } + + getColumn(colIndex) { + return this.datamanager.getColumn(colIndex); + } + + getColumns() { + return this.datamanager.getColumns(); + } + + getRows() { + return this.datamanager.getRows(); + } + + getCell(colIndex, rowIndex) { + return this.datamanager.getCell(colIndex, rowIndex); + } + + getColumnHeaderElement(colIndex) { + return this.columnmanager.getColumnHeaderElement(colIndex); + } + + getViewportHeight() { + if (!this.viewportHeight) { + this.viewportHeight = $.style(this.bodyScrollable, 'height'); + } + + return this.viewportHeight; + } + + sortColumn(colIndex, sortOrder) { + this.columnmanager.sortColumn(colIndex, sortOrder); + } + + removeColumn(colIndex) { + this.columnmanager.removeColumn(colIndex); + } + + scrollToLastColumn() { + this.datatableWrapper.scrollLeft = 9999; + } + + freeze() { + $.style(this.freezeContainer, { + display: '' + }); + } + + unfreeze() { + $.style(this.freezeContainer, { + display: 'none' + }); + } + + fireEvent(eventName, ...args) { + this.events[eventName].apply(this, args); + } + + log() { + if (this.options.enableLogs) { + console.log.apply(console, arguments); + } } - } } DataTable.instances = 0; diff --git a/docs/index.css b/docs/index.css index cd1fae4..786b20d 100644 --- a/docs/index.css +++ b/docs/index.css @@ -14,19 +14,27 @@ h1 { margin: 1rem 0; } -.hero { +h2 { + font-size: 2.5rem; +} + +.showcase { margin: 0 auto; padding: 4rem; text-align: center; } -.hero p { +.showcase p { font-size: 2rem; margin: 1rem 0; } -.datatable-1 { +.datatable-1, .datatable-2 { width: 735px; margin: 0 auto; margin-top: 2rem; +} + +.datatable-2 { + width: 934px; } \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 2368685..aef8ca7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -9,11 +9,16 @@ -
+

Frappe DataTable

A simple, modern and interactive datatable for the web

+
+

Tree Structure

+

Show tree structured rows in your table

+
+
diff --git a/docs/index.js b/docs/index.js index 47d374a..372b4af 100644 --- a/docs/index.js +++ b/docs/index.js @@ -1,13 +1,19 @@ /* global DataTable */ +/* eslint-disable no-unused-vars */ -const { columns, data } = getSampleData(); +const { + columns, + data +} = getSampleData(); let datatable1 = new DataTable('.datatable-1', { columns, data }); -console.log(datatable1); +let datatable2 = new DataTable('.datatable-2', Object.assign(getTreeData(), { + enableInlineFilters: true +})); function getSampleData(multiplier) { let columns = ['Name', 'Position', 'Office', 'Extn.', 'Start Date', 'Salary']; @@ -77,5 +83,609 @@ function getSampleData(multiplier) { }); } - return { columns, data }; + return { + columns, + data + }; +} + +function getTreeData() { + return { + columns: [{ + 'id': 'account', + 'content': 'Account' + }, { + 'id': 'opening_debit', + 'content': 'Opening (Dr)' + }, { + 'id': 'opening_credit', + 'content': 'Opening (Cr)' + }, { + 'id': 'debit', + 'content': 'Debit' + }, { + 'id': 'credit', + 'content': 'Credit' + }, { + 'id': 'closing_debit', + 'content': 'Closing (Dr)' + }, { + 'id': 'closing_credit', + 'content': 'Closing (Cr)' + }, { + 'id': 'currency', + 'content': 'Currency', + 'hidden': 1 + }], + data: [{ + 'account_name': 'Application of Funds (Assets)', + 'account': 'Application of Funds (Assets) - GTPL', + 'parent_account': null, + 'indent': 0, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 12023729.54, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 12023729.54, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Current Assets', + 'account': 'Current Assets - GTPL', + 'parent_account': 'Application of Funds (Assets) - GTPL', + 'indent': 1, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 13960649.54, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 13960649.54, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Accounts Receivable', + 'account': 'Accounts Receivable - GTPL', + 'parent_account': 'Current Assets - GTPL', + 'indent': 2, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 742790.474, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 742790.474, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Debtors', + 'account': 'Debtors - GTPL', + 'parent_account': 'Accounts Receivable - GTPL', + 'indent': 3, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 742790.474, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 742790.474, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Bank Accounts', + 'account': 'Bank Accounts - GTPL', + 'parent_account': 'Current Assets - GTPL', + 'indent': 2, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 280676.822, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 280676.822, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Corporation Bank', + 'account': 'Corporation Bank - GTPL', + 'parent_account': 'Bank Accounts - GTPL', + 'indent': 3, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 290676.822, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 290676.822, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'HDFC Bank', + 'account': 'HDFC Bank - GTPL', + 'parent_account': 'Bank Accounts - GTPL', + 'indent': 3, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 10000.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 10000.0, + 'has_value': true + }, { + 'account_name': 'Cash In Hand', + 'account': 'Cash In Hand - GTPL', + 'parent_account': 'Current Assets - GTPL', + 'indent': 2, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 229904.494, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 229904.494, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Cash', + 'account': 'Cash - GTPL', + 'parent_account': 'Cash In Hand - GTPL', + 'indent': 3, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 229904.494, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 229904.494, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Stock Assets', + 'account': 'Stock Assets - GTPL', + 'parent_account': 'Current Assets - GTPL', + 'indent': 2, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 12707277.75, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 12707277.75, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'All Warehouses', + 'account': 'All Warehouses - GTPL', + 'parent_account': 'Stock Assets - GTPL', + 'indent': 3, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 12707277.75, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 12707277.75, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Finished Goods', + 'account': 'Finished Goods - GTPL', + 'parent_account': 'All Warehouses - GTPL', + 'indent': 4, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 87320.3, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 87320.3, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Retail Stores', + 'account': 'Retail Stores - GTPL', + 'parent_account': 'All Warehouses - GTPL', + 'indent': 4, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 4540590.0, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 4540590.0, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Bandra Store', + 'account': 'Bandra Store - GTPL', + 'parent_account': 'Retail Stores - GTPL', + 'indent': 5, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 3246800.0, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 3246800.0, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Central Warehouse', + 'account': 'Central Warehouse - GTPL', + 'parent_account': 'Retail Stores - GTPL', + 'indent': 5, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 1236790.0, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 1236790.0, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Lower Parel Store', + 'account': 'Lower Parel Store - GTPL', + 'parent_account': 'Retail Stores - GTPL', + 'indent': 5, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 57000.0, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 57000.0, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Stores', + 'account': 'Stores - GTPL', + 'parent_account': 'All Warehouses - GTPL', + 'indent': 4, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 8016525.27, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 8016525.27, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Work In Progress', + 'account': 'Work In Progress - GTPL', + 'parent_account': 'All Warehouses - GTPL', + 'indent': 4, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 62842.18, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 62842.18, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Fixed Assets', + 'account': 'Fixed Assets - GTPL', + 'parent_account': 'Application of Funds (Assets) - GTPL', + 'indent': 1, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 19920.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 19920.0, + 'has_value': true + }, { + 'account_name': 'Electronic Equipments', + 'account': 'Electronic Equipments - GTPL', + 'parent_account': 'Fixed Assets - GTPL', + 'indent': 2, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 80.0, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 80.0, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'Furnitures and Fixtures', + 'account': 'Furnitures and Fixtures - GTPL', + 'parent_account': 'Fixed Assets - GTPL', + 'indent': 2, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 20000.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 20000.0, + 'has_value': true + }, { + 'account_name': 'Temporary Accounts', + 'account': 'Temporary Accounts - GTPL', + 'parent_account': 'Application of Funds (Assets) - GTPL', + 'indent': 1, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 1917000.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 1917000.0, + 'has_value': true + }, { + 'account_name': 'Temporary Opening', + 'account': 'Temporary Opening - GTPL', + 'parent_account': 'Temporary Accounts - GTPL', + 'indent': 2, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 1917000.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 1917000.0, + 'has_value': true + }, { + 'account_name': 'Source of Funds (Liabilities)', + 'account': 'Source of Funds (Liabilities) - GTPL', + 'parent_account': null, + 'indent': 0, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 2371628.002, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 2371628.002, + 'has_value': true + }, { + 'account_name': 'Current Liabilities', + 'account': 'Current Liabilities - GTPL', + 'parent_account': 'Source of Funds (Liabilities) - GTPL', + 'indent': 1, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 2371628.002, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 2371628.002, + 'has_value': true + }, { + 'account_name': 'Accounts Payable', + 'account': 'Accounts Payable - GTPL', + 'parent_account': 'Current Liabilities - GTPL', + 'indent': 2, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 368311.85, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 368311.85, + 'has_value': true + }, { + 'account_name': 'Creditors', + 'account': 'Creditors - GTPL', + 'parent_account': 'Accounts Payable - GTPL', + 'indent': 3, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 194871.85, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 194871.85, + 'has_value': true + }, { + 'account_name': 'Salary Payable', + 'account': 'Salary Payable - GTPL', + 'parent_account': 'Accounts Payable - GTPL', + 'indent': 3, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 173440.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 173440.0, + 'has_value': true + }, { + 'account_name': 'Duties and Taxes', + 'account': 'Duties and Taxes - GTPL', + 'parent_account': 'Current Liabilities - GTPL', + 'indent': 2, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 150146.822, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 150146.822, + 'has_value': true + }, { + 'account_name': 'CGST', + 'account': 'CGST - GTPL', + 'parent_account': 'Duties and Taxes - GTPL', + 'indent': 3, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 51479.591, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 51479.591, + 'has_value': true + }, { + 'account_name': 'IGST', + 'account': 'IGST - GTPL', + 'parent_account': 'Duties and Taxes - GTPL', + 'indent': 3, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 1944.0, + 'opening_credit': 0.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 1944.0, + 'closing_credit': 0.0, + 'has_value': true + }, { + 'account_name': 'SGST', + 'account': 'SGST - GTPL', + 'parent_account': 'Duties and Taxes - GTPL', + 'indent': 3, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 97711.231, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 97711.231, + 'has_value': true + }, { + 'account_name': 'UGST', + 'account': 'UGST - GTPL', + 'parent_account': 'Duties and Taxes - GTPL', + 'indent': 3, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 2900.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 2900.0, + 'has_value': true + }, { + 'account_name': 'Stock Liabilities', + 'account': 'Stock Liabilities - GTPL', + 'parent_account': 'Current Liabilities - GTPL', + 'indent': 2, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 1853169.33, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 1853169.33, + 'has_value': true + }, { + 'account_name': 'Stock Received But Not Billed', + 'account': 'Stock Received But Not Billed - GTPL', + 'parent_account': 'Stock Liabilities - GTPL', + 'indent': 3, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 1853169.33, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 1853169.33, + 'has_value': true + }, { + 'account_name': 'Equity', + 'account': 'Equity - GTPL', + 'parent_account': null, + 'indent': 0, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 10000.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 10000.0, + 'has_value': true + }, { + 'account_name': 'Capital Stock', + 'account': 'Capital Stock - GTPL', + 'parent_account': 'Equity - GTPL', + 'indent': 1, + 'from_date': '2018-04-01', + 'to_date': '2019-03-31', + 'currency': 'INR', + 'opening_debit': 0.0, + 'opening_credit': 10000.0, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 0.0, + 'closing_credit': 10000.0, + 'has_value': true + }, {}, { + 'account': 'Total', + 'account_name': 'Total', + 'warn_if_negative': true, + 'opening_debit': 32260956.43, + 'opening_credit': 22618854.891999997, + 'debit': 0.0, + 'credit': 0.0, + 'closing_debit': 32260956.43, + 'closing_credit': 22618854.891999997, + 'parent_account': null, + 'indent': 0, + 'has_value': true, + 'currency': 'INR' + }] + }; }