diff --git a/.eslintignore b/.eslintignore index 53c37a1..52bb028 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -dist \ No newline at end of file +dist +docs/frappe-datatable.js \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 91703fe..7ea222e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -55,7 +55,7 @@ "generator-star-spacing": [2, "both"], "guard-for-in": 0, "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], - "indent": [2, 2, { "SwitchCase": 1 }], + "indent": [2, 4, { "SwitchCase": 1 }], "key-spacing": [2, { "beforeColon": false, "afterColon": true }], "keyword-spacing": [2, {"before": true, "after": true}], "linebreak-style": 0, diff --git a/dist/frappe-datatable.cjs.css b/dist/frappe-datatable.cjs.css new file mode 100644 index 0000000..30dd637 --- /dev/null +++ b/dist/frappe-datatable.cjs.css @@ -0,0 +1,255 @@ +/* This file is processed by postcss */ +/* variables */ + +.data-table { + + /* styling */ + width: 100%; + position: relative; + overflow: auto; +} + +/* resets */ + +.data-table *, .data-table *::after, .data-table *::before { + -webkit-box-sizing: border-box; + box-sizing: border-box; + } + +.data-table button, .data-table input { + overflow: visible; + font-family: inherit; + font-size: inherit; + line-height: inherit; + margin: 0; + padding: 0; + } + +.data-table .input-style { + outline: none; + width: 100%; + border: none; + } + +.data-table *, .data-table *:focus { + outline: none; + border-radius: 0px; + -webkit-box-shadow: none; + box-shadow: none; + } + +.data-table table { + border-collapse: collapse; + } + +.data-table table td { + padding: 0; + border: 1px solid #d1d8dd; + } + +.data-table thead td { + border-bottom-width: 1px; + } + +.data-table .freeze-container { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -ms-flex-line-pack: center; + align-content: center; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: #f5f7fa; + opacity: 0.5; + font-size: 2em; + } + +.data-table .freeze-container span { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); + } + +.data-table .trash-container { + position: absolute; + bottom: 0; + left: 30%; + right: 30%; + height: 70px; + background: palevioletred; + opacity: 0.5; + } + +.data-table .hide { + display: none; + } + +.body-scrollable { + max-height: 500px; + overflow: auto; + border-bottom: 1px solid #d1d8dd; +} + +.body-scrollable.row-highlight-all .data-table-row:not(.row-unhighlight) { + background-color: #f5f7fa; + } + +.data-table-header { + position: absolute; + top: 0; + left: 0; + background-color: white; + font-weight: bold; +} + +.data-table-header .content span:not(.column-resizer) { + cursor: pointer; + } + +.data-table-header .column-resizer { + display: none; + position: absolute; + right: 0; + top: 0; + width: 4px; + width: 0.25rem; + height: 100%; + background-color: rgb(82, 146, 247); + cursor: col-resize; + } + +.data-table-header .data-table-dropdown { + position: absolute; + right: 10px; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: top; + text-align: left; + } + +.data-table-header .data-table-dropdown.is-active .data-table-dropdown-list { + display: block; + } + +.data-table-header .data-table-dropdown.is-active .data-table-dropdown-toggle { + display: block; + } + +.data-table-header .data-table-dropdown-toggle { + display: none; + background-color: transparent; + border: none; + } + +.data-table-header .data-table-dropdown-list { + display: none; + font-weight: normal; + + position: absolute; + min-width: 128px; + min-width: 8rem; + top: 100%; + right: 0; + z-index: 1; + background-color: white; + border-radius: 3px; + -webkit-box-shadow: 0 2px 3px rgba(10, 10, 10, .1), 0 0 0 1px rgba(10, 10, 10, .1); + box-shadow: 0 2px 3px rgba(10, 10, 10, .1), 0 0 0 1px rgba(10, 10, 10, .1); + padding-bottom: 8px; + padding-bottom: 0.5rem; + padding-top: 8px; + padding-top: 0.5rem; + } + +.data-table-header .data-table-dropdown-list> div { + padding: 8px 16px; + padding: 0.5rem 1rem; + } + +.data-table-header .data-table-dropdown-list> div:hover { + background-color: #f5f7fa; + } + +.data-table-header .data-table-col.remove-column { + background-color: #FD8B8B; + -webkit-transition: 300ms background-color ease-in-out; + transition: 300ms background-color ease-in-out; + } + +.data-table-header .data-table-col.sortable-chosen { + background-color: #f5f7fa; + } + +.data-table-col { + position: relative; +} + +.data-table-col .content { + padding: 8px; + padding: 0.5rem; + border: 2px solid transparent; + } + +.data-table-col .content.ellipsis { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + +.data-table-col .edit-cell { + display: none; + padding: 8px; + padding: 0.5rem; + background: #fff; + z-index: 1; + height: 100%; + } + +.data-table-col.selected .content { + border: 2px solid rgb(82, 146, 247); + } + +.data-table-col.editing .content { + display: none; + } + +.data-table-col.editing .edit-cell { + border: 2px solid rgb(82, 146, 247); + display: block; + } + +.data-table-col.highlight { + background-color: #f5f7fa; + } + +.data-table-col:hover .column-resizer { + display: inline-block; + } + +.data-table-col:hover .data-table-dropdown-toggle { + display: block; + } + +.data-table-row.row-highlight { + background-color: #f5f7fa; + } + +.noselect { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +body.data-table-resize { + cursor: col-resize; +} \ No newline at end of file diff --git a/dist/frappe-datatable.cjs.js b/dist/frappe-datatable.cjs.js index 0d523be..e194a4e 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; + }); }; /** @@ -720,26 +730,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; } @@ -753,62 +763,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; @@ -816,936 +826,985 @@ 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.forEach((row, index) => { + row.forEach(cell => { + if (cell.colIndex === srNoColIndex) { + cell.content = (index + 1) + ''; + } + }); + }); + } + } - 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('')}
@@ -1753,603 +1812,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} @@ -2358,704 +2463,719 @@ 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 { - constructor(instance) { - this.instance = instance; + constructor(instance) { + this.instance = instance; - linkProperties(this, this.instance, [ - 'options', 'datamanager', 'columnmanager', - 'header', 'bodyScrollable', 'getColumn' - ]); + linkProperties(this, this.instance, [ + 'options', 'datamanager', 'columnmanager', + 'header', 'bodyScrollable', 'getColumn' + ]); - this.scopeClass = 'datatable-instance-' + instance.constructor.instances; - instance.datatableWrapper.classList.add(this.scopeClass); + this.scopeClass = 'datatable-instance-' + instance.constructor.instances; + instance.datatableWrapper.classList.add(this.scopeClass); - const styleEl = document.createElement('style'); - instance.wrapper.insertBefore(styleEl, instance.datatableWrapper); - this.styleEl = styleEl; - this.styleSheet = styleEl.sheet; + const styleEl = document.createElement('style'); + instance.wrapper.insertBefore(styleEl, instance.datatableWrapper); + this.styleEl = styleEl; + this.styleSheet = styleEl.sheet; - this.bindResizeWindow(); - } + this.bindResizeWindow(); + } + + bindResizeWindow() { + if (this.options.layout === 'fluid') { + $.on(window, 'resize', throttle$1(() => { + this.distributeRemainingWidth(); + this.refreshColumnWidth(); + this.setBodyStyle(); + }, 300)); + } + } + + destroy() { + this.styleEl.remove(); + } + + setStyle(selector, styleMap, index = -1) { + const styles = Object.keys(styleMap) + .map(prop => { + if (!prop.includes('-')) { + prop = camelCaseToDash(prop); + } + return `${prop}:${styleMap[prop]};`; + }) + .join(''); + let prefixedSelector = selector + .split(',') + .map(r => `.${this.scopeClass} ${r}`) + .join(','); + + let ruleString = `${prefixedSelector} { ${styles} }`; + + let _index = this.styleSheet.cssRules.length; + if (index !== -1) { + this.styleSheet.deleteRule(index); + _index = index; + } + + this.styleSheet.insertRule(ruleString, _index); + return _index; + } + + setDimensions() { + this.setHeaderStyle(); + + this.setupMinWidth(); + this.setupNaturalColumnWidth(); + this.setupColumnWidth(); - bindResizeWindow() { - if (this.options.layout === 'fluid') { - $.on(window, 'resize', throttle$1(() => { this.distributeRemainingWidth(); - this.refreshColumnWidth(); + this.setColumnStyle(); + this.setDefaultCellHeight(); this.setBodyStyle(); - }, 300)); - } - } - - destroy() { - this.styleEl.remove(); - } - - setStyle(rule, styleMap, index = -1) { - const styles = Object.keys(styleMap) - .map(prop => { - if (!prop.includes('-')) { - prop = camelCaseToDash(prop); - } - return `${prop}:${styleMap[prop]};`; - }) - .join(''); - let ruleString = `.${this.scopeClass} ${rule} { ${styles} }`; - - let _index = this.styleSheet.cssRules.length; - if (index !== -1) { - this.styleSheet.deleteRule(index); - _index = index; } - this.styleSheet.insertRule(ruleString, _index); - return _index; - } + setHeaderStyle() { + if (this.options.layout === 'fluid') { + // setting width as 0 will ensure that the + // header doesn't take the available space + $.style(this.header, { + width: 0 + }); + } - setDimensions() { - this.setHeaderStyle(); + $.style(this.header, { + margin: 0 + }); - this.setupMinWidth(); - this.setupNaturalColumnWidth(); - this.setupColumnWidth(); + // don't show resize cursor on nonResizable columns + const nonResizableColumnsSelector = this.datamanager.getColumns() + .filter(col => col.resizable === false) + .map(col => col.colIndex) + .map(i => `.data-table-header [data-col-index="${i}"]`) + .join(); - this.distributeRemainingWidth(); - this.setColumnStyle(); - this.setDefaultCellHeight(); - this.setBodyStyle(); - } - - setHeaderStyle() { - if (this.options.layout === 'fluid') { - // setting width as 0 will ensure that the - // header doesn't take the available space - $.style(this.header, { - width: 0 - }); + this.setStyle(nonResizableColumnsSelector, { + cursor: 'pointer' + }); } - $.style(this.header, { - margin: 0 - }); + setupMinWidth() { + $.each('.data-table-col[data-is-header]', this.header).map(col => { + const width = $.style($('.content', col), 'width'); + const { + colIndex + } = $.data(col); + const column = this.getColumn(colIndex); - // don't show resize cursor on nonResizable columns - const nonResizableColumnsSelector = this.datamanager.getColumns() - .filter(col => col.resizable === false) - .map(col => col.colIndex) - .map(i => `.data-table-header [data-col-index="${i}"]`) - .join(); - - this.setStyle(nonResizableColumnsSelector, { - cursor: 'pointer' - }); - } - - setupMinWidth() { - $.each('.data-table-col[data-is-header]', this.header).map(col => { - const width = $.style($('.content', col), 'width'); - const { - colIndex - } = $.data(col); - const column = this.getColumn(colIndex); - - if (!column.minWidth) { - // only set this once - column.minWidth = width; - } - }); - } - - setupNaturalColumnWidth() { - if (!$('.data-table-row')) return; - - // set initial width as naturally calculated by table's first row - $.each('.data-table-row[data-row-index="0"] .data-table-col', this.bodyScrollable).map($cell => { - const { - colIndex - } = $.data($cell); - const column = this.datamanager.getColumn(colIndex); - - let naturalWidth = $.style($('.content', $cell), 'width'); - - if (column.id === '_rowIndex') { - // width based on rowCount - const rowCount = this.datamanager.getRowCount(); - const digits = (rowCount + '').length; - if (digits > 2) { - naturalWidth = naturalWidth + ((digits - 2) * 8); - } - } - - column.naturalWidth = naturalWidth; - }); - } - - setupColumnWidth() { - this.datamanager.getColumns() - .map(column => { - if (!column.width) { - column.width = column.naturalWidth; - } - if (column.width < column.minWidth) { - column.width = column.minWidth; - } - }); - } - - distributeRemainingWidth() { - if (this.options.layout !== 'fluid') return; - - const wrapperWidth = $.style(this.instance.datatableWrapper, 'width'); - const headerWidth = $.style(this.header, 'width'); - const resizableColumns = this.datamanager.getColumns().filter(col => col.resizable); - const deltaWidth = (wrapperWidth - headerWidth) / resizableColumns.length; - - resizableColumns.map(col => { - const width = $.style(this.getColumnHeaderElement(col.colIndex), 'width'); - let finalWidth = Math.floor(width + deltaWidth) - 2; - - this.datamanager.updateColumn(col.colIndex, { - width: finalWidth - }); - }); - } - - setDefaultCellHeight() { - if (this.__cellHeightSet) return; - const height = this.options.cellHeight || $.style($('.data-table-col', this.instance.datatableWrapper), 'height'); - if (height) { - this.setCellHeight(height); - this.__cellHeightSet = true; + if (!column.minWidth) { + // only set this once + column.minWidth = width; + } + }); } - } - setCellHeight(height) { - this.setStyle('.data-table-col .content', { - height: height + 'px' - }); - this.setStyle('.data-table-col .edit-cell', { - height: height + 'px' - }); - } + setupNaturalColumnWidth() { + if (!$('.data-table-row')) return; - setColumnStyle() { - // align columns - this.datamanager.getColumns() - .map(column => { - // alignment - if (['left', 'center', 'right'].includes(column.align)) { - this.setStyle(`[data-col-index="${column.colIndex}"]`, { - 'text-align': column.align - }); + // set initial width as naturally calculated by table's first row + $.each('.data-table-row[data-row-index="0"] .data-table-col', this.bodyScrollable).map($cell => { + const { + colIndex + } = $.data($cell); + const column = this.datamanager.getColumn(colIndex); + + let naturalWidth = $.style($('.content', $cell), 'width'); + + if (column.id === '_rowIndex') { + // width based on rowCount + const rowCount = this.datamanager.getRowCount(); + const digits = (rowCount + '').length; + if (digits > 1) { + naturalWidth = naturalWidth + ((digits - 1) * 8); + } + } + + column.naturalWidth = naturalWidth; + }); + } + + setupColumnWidth() { + this.datamanager.getColumns() + .map(column => { + if (!column.width) { + column.width = column.naturalWidth; + } + if (column.width < column.minWidth) { + column.width = column.minWidth; + } + }); + } + + distributeRemainingWidth() { + if (this.options.layout !== 'fluid') return; + + const wrapperWidth = $.style(this.instance.datatableWrapper, 'width'); + const headerWidth = $.style(this.header, 'width'); + const resizableColumns = this.datamanager.getColumns().filter(col => col.resizable); + const deltaWidth = (wrapperWidth - headerWidth) / resizableColumns.length; + + resizableColumns.map(col => { + const width = $.style(this.getColumnHeaderElement(col.colIndex), 'width'); + let finalWidth = Math.floor(width + deltaWidth) - 2; + + this.datamanager.updateColumn(col.colIndex, { + width: finalWidth + }); + }); + } + + setDefaultCellHeight() { + if (this.__cellHeightSet) return; + const height = this.options.cellHeight || + $.style($('.data-table-col', this.instance.datatableWrapper), 'height'); + if (height) { + this.setCellHeight(height); + this.__cellHeightSet = true; } - // width - this.columnmanager.setColumnHeaderWidth(column.colIndex); - this.columnmanager.setColumnWidth(column.colIndex); - }); - this.setBodyStyle(); - } + } - refreshColumnWidth() { - this.datamanager.getColumns() - .map(column => { - this.columnmanager.setColumnHeaderWidth(column.colIndex); - this.columnmanager.setColumnWidth(column.colIndex); - }); - } + setCellHeight(height) { + this.setStyle('.data-table-col .content', { + height: height + 'px' + }); + this.setStyle('.data-table-col .edit-cell', { + height: height + 'px' + }); + } - setBodyStyle() { - const width = $.style(this.header, 'width'); + setColumnStyle() { + // align columns + this.datamanager.getColumns() + .map(column => { + // alignment + if (['left', 'center', 'right'].includes(column.align)) { + this.setStyle(`[data-col-index="${column.colIndex}"]`, { + 'text-align': column.align + }); + } + // width + this.columnmanager.setColumnHeaderWidth(column.colIndex); + this.columnmanager.setColumnWidth(column.colIndex); + }); + this.setBodyStyle(); + } - $.style(this.bodyScrollable, { - width: width + 'px' - }); + refreshColumnWidth() { + this.datamanager.getColumns() + .map(column => { + this.columnmanager.setColumnHeaderWidth(column.colIndex); + this.columnmanager.setColumnWidth(column.colIndex); + }); + } - $.style(this.bodyScrollable, { - marginTop: $.style(this.header, 'height') + 'px' - }); + setBodyStyle() { + const width = $.style(this.header, 'width'); - $.style($('table', this.bodyScrollable), { - margin: 0 - }); - } + $.style(this.bodyScrollable, { + width: width + 'px' + }); - getColumnHeaderElement(colIndex) { - colIndex = +colIndex; - if (colIndex < 0) return null; - return $(`.data-table-col[data-col-index="${colIndex}"]`, this.header); - } + $.style(this.bodyScrollable, { + marginTop: $.style(this.header, 'height') + 'px' + }); + + $.style($('table', this.bodyScrollable), { + margin: 0 + }); + } + + getColumnHeaderElement(colIndex) { + colIndex = +colIndex; + if (colIndex < 0) return null; + return $(`.data-table-col[data-col-index="${colIndex}"]`, this.header); + } } 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 = `
@@ -3069,110 +3189,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 3bdc2fe..fab99f7 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; + }); }; /** @@ -719,26 +729,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; } @@ -752,62 +762,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; @@ -815,936 +825,985 @@ 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.forEach((row, index) => { + row.forEach(cell => { + if (cell.colIndex === srNoColIndex) { + cell.content = (index + 1) + ''; + } + }); + }); + } + } - 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('')}
@@ -1752,603 +1811,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} @@ -2357,704 +2462,719 @@ 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 { - constructor(instance) { - this.instance = instance; + constructor(instance) { + this.instance = instance; - linkProperties(this, this.instance, [ - 'options', 'datamanager', 'columnmanager', - 'header', 'bodyScrollable', 'getColumn' - ]); + linkProperties(this, this.instance, [ + 'options', 'datamanager', 'columnmanager', + 'header', 'bodyScrollable', 'getColumn' + ]); - this.scopeClass = 'datatable-instance-' + instance.constructor.instances; - instance.datatableWrapper.classList.add(this.scopeClass); + this.scopeClass = 'datatable-instance-' + instance.constructor.instances; + instance.datatableWrapper.classList.add(this.scopeClass); - const styleEl = document.createElement('style'); - instance.wrapper.insertBefore(styleEl, instance.datatableWrapper); - this.styleEl = styleEl; - this.styleSheet = styleEl.sheet; + const styleEl = document.createElement('style'); + instance.wrapper.insertBefore(styleEl, instance.datatableWrapper); + this.styleEl = styleEl; + this.styleSheet = styleEl.sheet; - this.bindResizeWindow(); - } + this.bindResizeWindow(); + } + + bindResizeWindow() { + if (this.options.layout === 'fluid') { + $.on(window, 'resize', throttle$1(() => { + this.distributeRemainingWidth(); + this.refreshColumnWidth(); + this.setBodyStyle(); + }, 300)); + } + } + + destroy() { + this.styleEl.remove(); + } + + setStyle(selector, styleMap, index = -1) { + const styles = Object.keys(styleMap) + .map(prop => { + if (!prop.includes('-')) { + prop = camelCaseToDash(prop); + } + return `${prop}:${styleMap[prop]};`; + }) + .join(''); + let prefixedSelector = selector + .split(',') + .map(r => `.${this.scopeClass} ${r}`) + .join(','); + + let ruleString = `${prefixedSelector} { ${styles} }`; + + let _index = this.styleSheet.cssRules.length; + if (index !== -1) { + this.styleSheet.deleteRule(index); + _index = index; + } + + this.styleSheet.insertRule(ruleString, _index); + return _index; + } + + setDimensions() { + this.setHeaderStyle(); + + this.setupMinWidth(); + this.setupNaturalColumnWidth(); + this.setupColumnWidth(); - bindResizeWindow() { - if (this.options.layout === 'fluid') { - $.on(window, 'resize', throttle$1(() => { this.distributeRemainingWidth(); - this.refreshColumnWidth(); + this.setColumnStyle(); + this.setDefaultCellHeight(); this.setBodyStyle(); - }, 300)); - } - } - - destroy() { - this.styleEl.remove(); - } - - setStyle(rule, styleMap, index = -1) { - const styles = Object.keys(styleMap) - .map(prop => { - if (!prop.includes('-')) { - prop = camelCaseToDash(prop); - } - return `${prop}:${styleMap[prop]};`; - }) - .join(''); - let ruleString = `.${this.scopeClass} ${rule} { ${styles} }`; - - let _index = this.styleSheet.cssRules.length; - if (index !== -1) { - this.styleSheet.deleteRule(index); - _index = index; } - this.styleSheet.insertRule(ruleString, _index); - return _index; - } + setHeaderStyle() { + if (this.options.layout === 'fluid') { + // setting width as 0 will ensure that the + // header doesn't take the available space + $.style(this.header, { + width: 0 + }); + } - setDimensions() { - this.setHeaderStyle(); + $.style(this.header, { + margin: 0 + }); - this.setupMinWidth(); - this.setupNaturalColumnWidth(); - this.setupColumnWidth(); + // don't show resize cursor on nonResizable columns + const nonResizableColumnsSelector = this.datamanager.getColumns() + .filter(col => col.resizable === false) + .map(col => col.colIndex) + .map(i => `.data-table-header [data-col-index="${i}"]`) + .join(); - this.distributeRemainingWidth(); - this.setColumnStyle(); - this.setDefaultCellHeight(); - this.setBodyStyle(); - } - - setHeaderStyle() { - if (this.options.layout === 'fluid') { - // setting width as 0 will ensure that the - // header doesn't take the available space - $.style(this.header, { - width: 0 - }); + this.setStyle(nonResizableColumnsSelector, { + cursor: 'pointer' + }); } - $.style(this.header, { - margin: 0 - }); + setupMinWidth() { + $.each('.data-table-col[data-is-header]', this.header).map(col => { + const width = $.style($('.content', col), 'width'); + const { + colIndex + } = $.data(col); + const column = this.getColumn(colIndex); - // don't show resize cursor on nonResizable columns - const nonResizableColumnsSelector = this.datamanager.getColumns() - .filter(col => col.resizable === false) - .map(col => col.colIndex) - .map(i => `.data-table-header [data-col-index="${i}"]`) - .join(); - - this.setStyle(nonResizableColumnsSelector, { - cursor: 'pointer' - }); - } - - setupMinWidth() { - $.each('.data-table-col[data-is-header]', this.header).map(col => { - const width = $.style($('.content', col), 'width'); - const { - colIndex - } = $.data(col); - const column = this.getColumn(colIndex); - - if (!column.minWidth) { - // only set this once - column.minWidth = width; - } - }); - } - - setupNaturalColumnWidth() { - if (!$('.data-table-row')) return; - - // set initial width as naturally calculated by table's first row - $.each('.data-table-row[data-row-index="0"] .data-table-col', this.bodyScrollable).map($cell => { - const { - colIndex - } = $.data($cell); - const column = this.datamanager.getColumn(colIndex); - - let naturalWidth = $.style($('.content', $cell), 'width'); - - if (column.id === '_rowIndex') { - // width based on rowCount - const rowCount = this.datamanager.getRowCount(); - const digits = (rowCount + '').length; - if (digits > 2) { - naturalWidth = naturalWidth + ((digits - 2) * 8); - } - } - - column.naturalWidth = naturalWidth; - }); - } - - setupColumnWidth() { - this.datamanager.getColumns() - .map(column => { - if (!column.width) { - column.width = column.naturalWidth; - } - if (column.width < column.minWidth) { - column.width = column.minWidth; - } - }); - } - - distributeRemainingWidth() { - if (this.options.layout !== 'fluid') return; - - const wrapperWidth = $.style(this.instance.datatableWrapper, 'width'); - const headerWidth = $.style(this.header, 'width'); - const resizableColumns = this.datamanager.getColumns().filter(col => col.resizable); - const deltaWidth = (wrapperWidth - headerWidth) / resizableColumns.length; - - resizableColumns.map(col => { - const width = $.style(this.getColumnHeaderElement(col.colIndex), 'width'); - let finalWidth = Math.floor(width + deltaWidth) - 2; - - this.datamanager.updateColumn(col.colIndex, { - width: finalWidth - }); - }); - } - - setDefaultCellHeight() { - if (this.__cellHeightSet) return; - const height = this.options.cellHeight || $.style($('.data-table-col', this.instance.datatableWrapper), 'height'); - if (height) { - this.setCellHeight(height); - this.__cellHeightSet = true; + if (!column.minWidth) { + // only set this once + column.minWidth = width; + } + }); } - } - setCellHeight(height) { - this.setStyle('.data-table-col .content', { - height: height + 'px' - }); - this.setStyle('.data-table-col .edit-cell', { - height: height + 'px' - }); - } + setupNaturalColumnWidth() { + if (!$('.data-table-row')) return; - setColumnStyle() { - // align columns - this.datamanager.getColumns() - .map(column => { - // alignment - if (['left', 'center', 'right'].includes(column.align)) { - this.setStyle(`[data-col-index="${column.colIndex}"]`, { - 'text-align': column.align - }); + // set initial width as naturally calculated by table's first row + $.each('.data-table-row[data-row-index="0"] .data-table-col', this.bodyScrollable).map($cell => { + const { + colIndex + } = $.data($cell); + const column = this.datamanager.getColumn(colIndex); + + let naturalWidth = $.style($('.content', $cell), 'width'); + + if (column.id === '_rowIndex') { + // width based on rowCount + const rowCount = this.datamanager.getRowCount(); + const digits = (rowCount + '').length; + if (digits > 1) { + naturalWidth = naturalWidth + ((digits - 1) * 8); + } + } + + column.naturalWidth = naturalWidth; + }); + } + + setupColumnWidth() { + this.datamanager.getColumns() + .map(column => { + if (!column.width) { + column.width = column.naturalWidth; + } + if (column.width < column.minWidth) { + column.width = column.minWidth; + } + }); + } + + distributeRemainingWidth() { + if (this.options.layout !== 'fluid') return; + + const wrapperWidth = $.style(this.instance.datatableWrapper, 'width'); + const headerWidth = $.style(this.header, 'width'); + const resizableColumns = this.datamanager.getColumns().filter(col => col.resizable); + const deltaWidth = (wrapperWidth - headerWidth) / resizableColumns.length; + + resizableColumns.map(col => { + const width = $.style(this.getColumnHeaderElement(col.colIndex), 'width'); + let finalWidth = Math.floor(width + deltaWidth) - 2; + + this.datamanager.updateColumn(col.colIndex, { + width: finalWidth + }); + }); + } + + setDefaultCellHeight() { + if (this.__cellHeightSet) return; + const height = this.options.cellHeight || + $.style($('.data-table-col', this.instance.datatableWrapper), 'height'); + if (height) { + this.setCellHeight(height); + this.__cellHeightSet = true; } - // width - this.columnmanager.setColumnHeaderWidth(column.colIndex); - this.columnmanager.setColumnWidth(column.colIndex); - }); - this.setBodyStyle(); - } + } - refreshColumnWidth() { - this.datamanager.getColumns() - .map(column => { - this.columnmanager.setColumnHeaderWidth(column.colIndex); - this.columnmanager.setColumnWidth(column.colIndex); - }); - } + setCellHeight(height) { + this.setStyle('.data-table-col .content', { + height: height + 'px' + }); + this.setStyle('.data-table-col .edit-cell', { + height: height + 'px' + }); + } - setBodyStyle() { - const width = $.style(this.header, 'width'); + setColumnStyle() { + // align columns + this.datamanager.getColumns() + .map(column => { + // alignment + if (['left', 'center', 'right'].includes(column.align)) { + this.setStyle(`[data-col-index="${column.colIndex}"]`, { + 'text-align': column.align + }); + } + // width + this.columnmanager.setColumnHeaderWidth(column.colIndex); + this.columnmanager.setColumnWidth(column.colIndex); + }); + this.setBodyStyle(); + } - $.style(this.bodyScrollable, { - width: width + 'px' - }); + refreshColumnWidth() { + this.datamanager.getColumns() + .map(column => { + this.columnmanager.setColumnHeaderWidth(column.colIndex); + this.columnmanager.setColumnWidth(column.colIndex); + }); + } - $.style(this.bodyScrollable, { - marginTop: $.style(this.header, 'height') + 'px' - }); + setBodyStyle() { + const width = $.style(this.header, 'width'); - $.style($('table', this.bodyScrollable), { - margin: 0 - }); - } + $.style(this.bodyScrollable, { + width: width + 'px' + }); - getColumnHeaderElement(colIndex) { - colIndex = +colIndex; - if (colIndex < 0) return null; - return $(`.data-table-col[data-col-index="${colIndex}"]`, this.header); - } + $.style(this.bodyScrollable, { + marginTop: $.style(this.header, 'height') + 'px' + }); + + $.style($('table', this.bodyScrollable), { + margin: 0 + }); + } + + getColumnHeaderElement(colIndex) { + colIndex = +colIndex; + if (colIndex < 0) return null; + return $(`.data-table-col[data-col-index="${colIndex}"]`, this.header); + } } 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 = `
@@ -3068,110 +3188,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 new file mode 100644 index 0000000..30dd637 --- /dev/null +++ b/docs/frappe-datatable.css @@ -0,0 +1,255 @@ +/* This file is processed by postcss */ +/* variables */ + +.data-table { + + /* styling */ + width: 100%; + position: relative; + overflow: auto; +} + +/* resets */ + +.data-table *, .data-table *::after, .data-table *::before { + -webkit-box-sizing: border-box; + box-sizing: border-box; + } + +.data-table button, .data-table input { + overflow: visible; + font-family: inherit; + font-size: inherit; + line-height: inherit; + margin: 0; + padding: 0; + } + +.data-table .input-style { + outline: none; + width: 100%; + border: none; + } + +.data-table *, .data-table *:focus { + outline: none; + border-radius: 0px; + -webkit-box-shadow: none; + box-shadow: none; + } + +.data-table table { + border-collapse: collapse; + } + +.data-table table td { + padding: 0; + border: 1px solid #d1d8dd; + } + +.data-table thead td { + border-bottom-width: 1px; + } + +.data-table .freeze-container { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -ms-flex-line-pack: center; + align-content: center; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: #f5f7fa; + opacity: 0.5; + font-size: 2em; + } + +.data-table .freeze-container span { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); + } + +.data-table .trash-container { + position: absolute; + bottom: 0; + left: 30%; + right: 30%; + height: 70px; + background: palevioletred; + opacity: 0.5; + } + +.data-table .hide { + display: none; + } + +.body-scrollable { + max-height: 500px; + overflow: auto; + border-bottom: 1px solid #d1d8dd; +} + +.body-scrollable.row-highlight-all .data-table-row:not(.row-unhighlight) { + background-color: #f5f7fa; + } + +.data-table-header { + position: absolute; + top: 0; + left: 0; + background-color: white; + font-weight: bold; +} + +.data-table-header .content span:not(.column-resizer) { + cursor: pointer; + } + +.data-table-header .column-resizer { + display: none; + position: absolute; + right: 0; + top: 0; + width: 4px; + width: 0.25rem; + height: 100%; + background-color: rgb(82, 146, 247); + cursor: col-resize; + } + +.data-table-header .data-table-dropdown { + position: absolute; + right: 10px; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: top; + text-align: left; + } + +.data-table-header .data-table-dropdown.is-active .data-table-dropdown-list { + display: block; + } + +.data-table-header .data-table-dropdown.is-active .data-table-dropdown-toggle { + display: block; + } + +.data-table-header .data-table-dropdown-toggle { + display: none; + background-color: transparent; + border: none; + } + +.data-table-header .data-table-dropdown-list { + display: none; + font-weight: normal; + + position: absolute; + min-width: 128px; + min-width: 8rem; + top: 100%; + right: 0; + z-index: 1; + background-color: white; + border-radius: 3px; + -webkit-box-shadow: 0 2px 3px rgba(10, 10, 10, .1), 0 0 0 1px rgba(10, 10, 10, .1); + box-shadow: 0 2px 3px rgba(10, 10, 10, .1), 0 0 0 1px rgba(10, 10, 10, .1); + padding-bottom: 8px; + padding-bottom: 0.5rem; + padding-top: 8px; + padding-top: 0.5rem; + } + +.data-table-header .data-table-dropdown-list> div { + padding: 8px 16px; + padding: 0.5rem 1rem; + } + +.data-table-header .data-table-dropdown-list> div:hover { + background-color: #f5f7fa; + } + +.data-table-header .data-table-col.remove-column { + background-color: #FD8B8B; + -webkit-transition: 300ms background-color ease-in-out; + transition: 300ms background-color ease-in-out; + } + +.data-table-header .data-table-col.sortable-chosen { + background-color: #f5f7fa; + } + +.data-table-col { + position: relative; +} + +.data-table-col .content { + padding: 8px; + padding: 0.5rem; + border: 2px solid transparent; + } + +.data-table-col .content.ellipsis { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + +.data-table-col .edit-cell { + display: none; + padding: 8px; + padding: 0.5rem; + background: #fff; + z-index: 1; + height: 100%; + } + +.data-table-col.selected .content { + border: 2px solid rgb(82, 146, 247); + } + +.data-table-col.editing .content { + display: none; + } + +.data-table-col.editing .edit-cell { + border: 2px solid rgb(82, 146, 247); + display: block; + } + +.data-table-col.highlight { + background-color: #f5f7fa; + } + +.data-table-col:hover .column-resizer { + display: inline-block; + } + +.data-table-col:hover .data-table-dropdown-toggle { + display: block; + } + +.data-table-row.row-highlight { + background-color: #f5f7fa; + } + +.noselect { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +body.data-table-resize { + cursor: col-resize; +} \ No newline at end of file diff --git a/docs/frappe-datatable.js b/docs/frappe-datatable.js new file mode 100644 index 0000000..fab99f7 --- /dev/null +++ b/docs/frappe-datatable.js @@ -0,0 +1,3332 @@ +var DataTable = (function (Sortable,Clusterize) { +'use strict'; + +Sortable = Sortable && Sortable.hasOwnProperty('default') ? Sortable['default'] : Sortable; +Clusterize = Clusterize && Clusterize.hasOwnProperty('default') ? Clusterize['default'] : Clusterize; + +function $(expr, con) { + return typeof expr === 'string' ? + (con || document).querySelector(expr) : + expr || null; +} + +$.each = (expr, con) => { + return typeof expr === 'string' ? + Array.from((con || document).querySelectorAll(expr)) : + expr || null; +}; + +$.create = (tag, o) => { + let element = document.createElement(tag); + + for (let i in o) { + let val = o[i]; + + if (i === 'inside') { + $(val).appendChild(element); + } else + if (i === 'around') { + let ref = $(val); + ref.parentNode.insertBefore(element, ref); + element.appendChild(ref); + } else + if (i === 'styles') { + if (typeof val === 'object') { + Object.keys(val).map(prop => { + element.style[prop] = val[prop]; + }); + } + } else + if (i in element) { + element[i] = val; + } else { + element.setAttribute(i, val); + } + } + + return element; +}; + +$.on = (element, event, selector, callback) => { + if (!callback) { + callback = selector; + $.bind(element, event, callback); + } else { + $.delegate(element, event, selector, callback); + } +}; + +$.off = (element, event, handler) => { + element.removeEventListener(event, handler); +}; + +$.bind = (element, event, callback) => { + event.split(/\s+/).forEach(function (event) { + element.addEventListener(event, callback); + }); +}; + +$.delegate = (element, event, selector, callback) => { + element.addEventListener(event, function (e) { + const delegatedTarget = e.target.closest(selector); + if (delegatedTarget) { + e.delegatedTarget = delegatedTarget; + callback.call(this, e, delegatedTarget); + } + }); +}; + +$.unbind = (element, o) => { + if (element) { + for (let event in o) { + let callback = o[event]; + + event.split(/\s+/).forEach(function (event) { + element.removeEventListener(event, callback); + }); + } + } +}; + +$.fire = (target, type, properties) => { + let evt = document.createEvent('HTMLEvents'); + + evt.initEvent(type, true, true); + + for (let j in properties) { + evt[j] = properties[j]; + } + + return target.dispatchEvent(evt); +}; + +$.data = (element, attrs) => { // eslint-disable-line + if (!attrs) { + return element.dataset; + } + + for (const attr in attrs) { + element.dataset[attr] = attrs[attr]; + } +}; + +$.style = (elements, styleMap) => { // eslint-disable-line + + if (typeof styleMap === 'string') { + return $.getStyle(elements, styleMap); + } + + if (!Array.isArray(elements)) { + elements = [elements]; + } + + elements.map(element => { + for (const prop in styleMap) { + element.style[prop] = styleMap[prop]; + } + }); +}; + +$.removeStyle = (elements, styleProps) => { + if (!Array.isArray(elements)) { + elements = [elements]; + } + + if (!Array.isArray(styleProps)) { + styleProps = [styleProps]; + } + + elements.map(element => { + for (const prop of styleProps) { + element.style[prop] = ''; + } + }); +}; + +$.getStyle = (element, prop) => { + let val = getComputedStyle(element)[prop]; + + if (['width', 'height'].includes(prop)) { + val = parseFloat(val); + } + + return val; +}; + +$.closest = (selector, element) => { + if (!element) return null; + + if (element.matches(selector)) { + return element; + } + + return $.closest(selector, element.parentNode); +}; + +$.inViewport = (el, parentEl) => { + const { + top, + left, + bottom, + right + } = el.getBoundingClientRect(); + const { + top: pTop, + left: pLeft, + bottom: pBottom, + right: pRight + } = parentEl.getBoundingClientRect(); + + return top >= pTop && left >= pLeft && bottom <= pBottom && right <= pRight; +}; + +$.scrollTop = function scrollTop(element, pixels) { + requestAnimationFrame(() => { + element.scrollTop = pixels; + }); +}; + +/** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ +function isObject(value) { + var type = typeof value; + return value != null && (type == 'object' || type == 'function'); +} + +var isObject_1 = isObject; + +var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + +/** Detect free variable `global` from Node.js. */ +var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; + +var _freeGlobal = freeGlobal; + +/** Detect free variable `self`. */ +var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + +/** Used as a reference to the global object. */ +var root = _freeGlobal || freeSelf || Function('return this')(); + +var _root = root; + +/** + * Gets the timestamp of the number of milliseconds that have elapsed since + * the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Date + * @returns {number} Returns the timestamp. + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => Logs the number of milliseconds it took for the deferred invocation. + */ +var now = function() { + return _root.Date.now(); +}; + +var now_1 = now; + +/** Built-in value references. */ +var Symbol = _root.Symbol; + +var _Symbol = Symbol; + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString = objectProto.toString; + +/** Built-in value references. */ +var symToStringTag = _Symbol ? _Symbol.toStringTag : undefined; + +/** + * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the raw `toStringTag`. + */ +function getRawTag(value) { + var isOwn = hasOwnProperty.call(value, symToStringTag), + tag = value[symToStringTag]; + + try { + value[symToStringTag] = undefined; + var unmasked = true; + } catch (e) {} + + var result = nativeObjectToString.call(value); + if (unmasked) { + if (isOwn) { + value[symToStringTag] = tag; + } else { + delete value[symToStringTag]; + } + } + return result; +} + +var _getRawTag = getRawTag; + +/** Used for built-in method references. */ +var objectProto$1 = Object.prototype; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString$1 = objectProto$1.toString; + +/** + * Converts `value` to a string using `Object.prototype.toString`. + * + * @private + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + */ +function objectToString(value) { + return nativeObjectToString$1.call(value); +} + +var _objectToString = objectToString; + +/** `Object#toString` result references. */ +var nullTag = '[object Null]'; +var undefinedTag = '[object Undefined]'; + +/** Built-in value references. */ +var symToStringTag$1 = _Symbol ? _Symbol.toStringTag : undefined; + +/** + * The base implementation of `getTag` without fallbacks for buggy environments. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ +function baseGetTag(value) { + if (value == null) { + return value === undefined ? undefinedTag : nullTag; + } + return (symToStringTag$1 && symToStringTag$1 in Object(value)) + ? _getRawTag(value) + : _objectToString(value); +} + +var _baseGetTag = baseGetTag; + +/** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ +function isObjectLike(value) { + return value != null && typeof value == 'object'; +} + +var isObjectLike_1 = isObjectLike; + +/** `Object#toString` result references. */ +var symbolTag = '[object Symbol]'; + +/** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ +function isSymbol(value) { + return typeof value == 'symbol' || + (isObjectLike_1(value) && _baseGetTag(value) == symbolTag); +} + +var isSymbol_1 = isSymbol; + +/** Used as references for various `Number` constants. */ +var NAN = 0 / 0; + +/** Used to match leading and trailing whitespace. */ +var reTrim = /^\s+|\s+$/g; + +/** Used to detect bad signed hexadecimal string values. */ +var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; + +/** Used to detect binary string values. */ +var reIsBinary = /^0b[01]+$/i; + +/** Used to detect octal string values. */ +var reIsOctal = /^0o[0-7]+$/i; + +/** Built-in method references without a dependency on `root`. */ +var freeParseInt = parseInt; + +/** + * Converts `value` to a number. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {number} Returns the number. + * @example + * + * _.toNumber(3.2); + * // => 3.2 + * + * _.toNumber(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toNumber(Infinity); + * // => Infinity + * + * _.toNumber('3.2'); + * // => 3.2 + */ +function toNumber(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol_1(value)) { + return NAN; + } + if (isObject_1(value)) { + var other = typeof value.valueOf == 'function' ? value.valueOf() : value; + value = isObject_1(other) ? (other + '') : other; + } + if (typeof value != 'string') { + return value === 0 ? value : +value; + } + value = value.replace(reTrim, ''); + var isBinary = reIsBinary.test(value); + return (isBinary || reIsOctal.test(value)) + ? freeParseInt(value.slice(2), isBinary ? 2 : 8) + : (reIsBadHex.test(value) ? NAN : +value); +} + +var toNumber_1 = toNumber; + +/** Error message constants. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeMax = Math.max; +var nativeMin = Math.min; + +/** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ +function debounce(func, wait, options) { + var lastArgs, + lastThis, + maxWait, + result, + timerId, + lastCallTime, + lastInvokeTime = 0, + leading = false, + maxing = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = toNumber_1(wait) || 0; + if (isObject_1(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? nativeMax(toNumber_1(options.maxWait) || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + var args = lastArgs, + thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime, + timeWaiting = wait - timeSinceLastCall; + + return maxing + ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) + : timeWaiting; + } + + function shouldInvoke(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || + (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired() { + var time = now_1(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(now_1()); + } + + function debounced() { + var time = now_1(), + isInvoking = shouldInvoke(time); + + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; +} + +var debounce_1 = debounce; + +/** Error message constants. */ +var FUNC_ERROR_TEXT$1 = 'Expected a function'; + +/** + * Creates a throttled function that only invokes `func` at most once per + * every `wait` milliseconds. The throttled function comes with a `cancel` + * method to cancel delayed `func` invocations and a `flush` method to + * immediately invoke them. Provide `options` to indicate whether `func` + * should be invoked on the leading and/or trailing edge of the `wait` + * timeout. The `func` is invoked with the last arguments provided to the + * throttled function. Subsequent calls to the throttled function return the + * result of the last `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the throttled function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.throttle` and `_.debounce`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to throttle. + * @param {number} [wait=0] The number of milliseconds to throttle invocations to. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=true] + * Specify invoking on the leading edge of the timeout. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // Avoid excessively updating the position while scrolling. + * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); + * + * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. + * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); + * jQuery(element).on('click', throttled); + * + * // Cancel the trailing throttled invocation. + * jQuery(window).on('popstate', throttled.cancel); + */ +function throttle(func, wait, options) { + var leading = true, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT$1); + } + if (isObject_1(options)) { + leading = 'leading' in options ? !!options.leading : leading; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + return debounce_1(func, wait, { + 'leading': leading, + 'maxWait': wait, + 'trailing': trailing + }); +} + +var throttle_1 = throttle; + +function camelCaseToDash(str) { + return str.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); +} + +function makeDataAttributeString(props) { + const keys = Object.keys(props); + + return keys + .map((key) => { + const _key = camelCaseToDash(key); + const val = props[key]; + + if (val === undefined) return ''; + return `data-${_key}="${val}" `; + }) + .join('') + .trim(); +} + +function getDefault(a, b) { + return a !== undefined ? a : b; +} + + + + + + + + + + + +function copyTextToClipboard(text) { + // https://stackoverflow.com/a/30810322/5353542 + var textArea = document.createElement('textarea'); + + // + // *** This styling is an extra step which is likely not required. *** + // + // Why is it here? To ensure: + // 1. the element is able to have focus and selection. + // 2. if element was to flash render it has minimal visual impact. + // 3. less flakyness with selection and copying which **might** occur if + // the textarea element is not visible. + // + // The likelihood is the element won't even render, not even a flash, + // so some of these are just precautions. However in IE the element + // is visible whilst the popup box asking the user for permission for + // the web page to copy to the clipboard. + // + + // Place in top-left corner of screen regardless of scroll position. + textArea.style.position = 'fixed'; + textArea.style.top = 0; + textArea.style.left = 0; + + // Ensure it has a small width and height. Setting to 1px / 1em + // doesn't work as this gives a negative w/h on some browsers. + textArea.style.width = '2em'; + textArea.style.height = '2em'; + + // We don't need padding, reducing the size if it does flash render. + textArea.style.padding = 0; + + // Clean up any borders. + textArea.style.border = 'none'; + textArea.style.outline = 'none'; + textArea.style.boxShadow = 'none'; + + // Avoid flash of white box if rendered for any reason. + textArea.style.background = 'transparent'; + + textArea.value = text; + + document.body.appendChild(textArea); + + textArea.select(); + + try { + document.execCommand('copy'); + } catch (err) { + console.log('Oops, unable to copy'); + } + + document.body.removeChild(textArea); +} + +function isNumeric(val) { + return !isNaN(val); +} + +let throttle$1 = throttle_1; + +let debounce$2 = debounce_1; + +function promisify(fn, context = null) { + return (...args) => { + return new Promise(resolve => { + setTimeout(() => { + const out = fn.apply(context, args); + resolve(out); + }, 0); + }); + }; +} + + + +function linkProperties(target, source, properties) { + const props = properties.reduce((acc, prop) => { + acc[prop] = { + get() { + return source[prop]; + } + }; + return acc; + }, {}); + Object.defineProperties(target, props); +} + +class DataManager { + constructor(options) { + this.options = options; + this.sortRows = promisify(this.sortRows, this); + this.switchColumn = promisify(this.switchColumn, this); + this.removeColumn = promisify(this.removeColumn, this); + this.filterRows = promisify(this.filterRows, this); + } + + init(data) { + if (!data) { + data = this.options.data; + } + + this.data = data; + + this.rowCount = 0; + this.columns = []; + this.rows = []; + + this.prepareColumns(); + this.prepareRows(); + + this.prepareNumericColumns(); + } + + // computed property + get currentSort() { + const col = this.columns.find(col => col.sortOrder !== 'none'); + return col || { + colIndex: -1, + sortOrder: 'none' + }; + } + + prepareColumns() { + this.columns = []; + this.validateColumns(); + this.prepareDefaultColumns(); + this.prepareHeader(); + } + + prepareDefaultColumns() { + if (this.options.addCheckboxColumn && !this.hasColumnById('_checkbox')) { + const cell = { + id: '_checkbox', + content: this.getCheckboxHTML(), + editable: false, + resizable: false, + sortable: false, + focusable: false, + dropdown: false, + width: 25 + }; + this.columns.push(cell); + } + + if (this.options.addSerialNoColumn && !this.hasColumnById('_rowIndex')) { + let cell = { + id: '_rowIndex', + content: '', + align: 'center', + editable: false, + resizable: false, + focusable: false, + dropdown: false + }; + + this.columns.push(cell); + } + } + + prepareHeader() { + let columns = this.columns.concat(this.options.columns); + const baseCell = { + isHeader: 1, + editable: true, + sortable: true, + resizable: true, + focusable: true, + dropdown: true, + width: null, + format: (value) => { + if (value === null || value === undefined) { + return ''; + } + return value + ''; + } + }; + + this.columns = columns + .map((cell, i) => this.prepareCell(cell, i)) + .map(col => Object.assign({}, baseCell, col)) + .map(col => { + col.id = col.id || col.content; + return col; + }); + } + + prepareCell(content, i) { + const cell = { + content: '', + align: 'left', + sortOrder: 'none', + colIndex: i, + column: this.columns[i] + }; + + if (content !== null && typeof content === 'object') { + // passed as column/header + Object.assign(cell, content); + } else { + cell.content = content; + } + + return cell; + } + + prepareNumericColumns() { + const row0 = this.getRow(0); + if (!row0) return; + this.columns = this.columns.map((column, i) => { + + const cellValue = row0[i].content; + if (!column.align && cellValue && isNumeric(cellValue)) { + column.align = 'right'; + } + + return column; + }); + } + + prepareRows() { + this.validateData(this.data); + + this.rows = this.data.map((d, i) => { + const index = this._getNextRowCount(); + + let row = []; + + if (Array.isArray(d)) { + // row is an array + if (this.options.addCheckboxColumn) { + row.push(this.getCheckboxHTML()); + } + if (this.options.addSerialNoColumn) { + row.push((index + 1) + ''); + } + row = row.concat(d); + + while (row.length < this.columns.length) { + row.push(''); + } + + } else { + // row is 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 + }); + }); + } + + prepareRow(row, props) { + const baseRowCell = { + rowIndex: props.rowIndex + }; + + row = row + .map((cell, i) => this.prepareCell(cell, i)) + .map(cell => Object.assign({}, baseRowCell, cell)); + + // monkey patched in array object + row.meta = props; + return row; + } + + validateColumns() { + const columns = this.options.columns; + if (!Array.isArray(columns)) { + throw new DataError('`columns` must be an array'); + } + + columns.forEach((column, i) => { + if (typeof column !== 'string' && typeof column !== 'object') { + throw new DataError(`column "${i}" must be a string or an object`); + } + }); + } + + validateData(data) { + if (Array.isArray(data) && + (data.length === 0 || Array.isArray(data[0]) || typeof data[0] === 'object')) { + return true; + } + throw new DataError('`data` must be an array of arrays or objects'); + } + + appendRows(rows) { + this.validateData(rows); + + this.rows = this.rows.concat(this.prepareRows(rows)); + } + + sortRows(colIndex, sortOrder = 'none') { + colIndex = +colIndex; + + // reset sortOrder and update for colIndex + this.getColumns() + .map(col => { + if (col.colIndex === colIndex) { + col.sortOrder = sortOrder; + } else { + col.sortOrder = 'none'; + } + }); + + this._sortRows(colIndex, sortOrder); + } + + _sortRows(colIndex, sortOrder) { + + if (this.currentSort.colIndex === colIndex) { + // reverse the array if only sortOrder changed + if ( + (this.currentSort.sortOrder === 'asc' && sortOrder === 'desc') || + (this.currentSort.sortOrder === 'desc' && sortOrder === 'asc') + ) { + this.reverseArray(this.rows); + this.currentSort.sortOrder = sortOrder; + return; + } + } + + this.rows.sort((a, b) => { + const _aIndex = a[0].rowIndex; + const _bIndex = b[0].rowIndex; + const _a = a[colIndex].content; + const _b = b[colIndex].content; + + if (sortOrder === 'none') { + return _aIndex - _bIndex; + } else if (sortOrder === 'asc') { + if (_a < _b) return -1; + if (_a > _b) return 1; + if (_a === _b) return 0; + } else if (sortOrder === 'desc') { + if (_a < _b) return 1; + if (_a > _b) return -1; + if (_a === _b) return 0; + } + return 0; + }); + + if (this.hasColumnById('_rowIndex')) { + // update row index + const srNoColIndex = this.getColumnIndexById('_rowIndex'); + this.rows.forEach((row, index) => { + row.forEach(cell => { + if (cell.colIndex === srNoColIndex) { + cell.content = (index + 1) + ''; + } + }); + }); + } + } + + reverseArray(array) { + let left = null; + let right = null; + let length = array.length; + + for (left = 0, right = length - 1; left < right; left += 1, right -= 1) { + const temporary = array[left]; + + array[left] = array[right]; + array[right] = temporary; + } + } + + switchColumn(index1, index2) { + // update columns + const temp = this.columns[index1]; + this.columns[index1] = this.columns[index2]; + this.columns[index2] = temp; + + this.columns[index1].colIndex = index1; + this.columns[index2].colIndex = index2; + + // update rows + this.rows = this.rows.map(row => { + const newCell1 = Object.assign({}, row[index1], { + colIndex: index2 + }); + const newCell2 = Object.assign({}, row[index2], { + colIndex: index1 + }); + + let newRow = row.map(cell => { + // make object copy + return Object.assign({}, cell); + }); + + newRow[index2] = newCell1; + newRow[index1] = newCell2; + + return newRow; + }); + } + + removeColumn(index) { + index = +index; + const filter = cell => cell.colIndex !== index; + const map = (cell, i) => Object.assign({}, cell, { + colIndex: i + }); + // update columns + this.columns = this.columns + .filter(filter) + .map(map); + + // update rows + this.rows = this.rows.map(row => { + const newRow = row + .filter(filter) + .map(map); + + return newRow; + }); + } + + updateRow(row, rowIndex) { + if (row.length < this.columns.length) { + if (this.hasColumnById('_rowIndex')) { + const val = (rowIndex + 1) + ''; + + row = [val].concat(row); + } + + if (this.hasColumnById('_checkbox')) { + const val = ''; + + row = [val].concat(row); + } + } + + const _row = this.prepareRow(row, rowIndex); + const index = this.rows.findIndex(row => row[0].rowIndex === rowIndex); + this.rows[index] = _row; + + return _row; + } + + updateCell(colIndex, rowIndex, options) { + let cell; + if (typeof colIndex === 'object') { + // cell object was passed, + // must have colIndex, rowIndex + cell = colIndex; + colIndex = cell.colIndex; + rowIndex = cell.rowIndex; + // the object passed must be merged with original cell + options = cell; + } + cell = this.getCell(colIndex, rowIndex); + + // mutate object directly + for (let key in options) { + const newVal = options[key]; + if (newVal !== undefined) { + cell[key] = newVal; + } + } + + return cell; + } + + updateColumn(colIndex, keyValPairs) { + const column = this.getColumn(colIndex); + for (let key in keyValPairs) { + const newVal = keyValPairs[key]; + if (newVal !== undefined) { + column[key] = newVal; + } + } + return column; + } + + filterRows(keyword, colIndex) { + let rowsToHide = []; + let rowsToShow = []; + const cells = this.rows.map(row => row[colIndex]); + + cells.forEach(cell => { + const hay = cell.content.toLowerCase(); + const needle = (keyword || '').toLowerCase(); + + if (!needle || hay.includes(needle)) { + rowsToShow.push(cell.rowIndex); + } else { + rowsToHide.push(cell.rowIndex); + } + }); + + return { + rowsToHide, + rowsToShow + }; + } + + getRowCount() { + return this.rowCount; + } + + _getNextRowCount() { + const val = this.rowCount; + + this.rowCount++; + return val; + } + + getRows(start, end) { + return this.rows.slice(start, end); + } + + getColumns(skipStandardColumns) { + let columns = this.columns; + + if (skipStandardColumns) { + columns = columns.slice(this.getStandardColumnCount()); + } + + return columns; + } + + getStandardColumnCount() { + if (this.options.addCheckboxColumn && this.options.addSerialNoColumn) { + return 2; + } + + if (this.options.addCheckboxColumn || this.options.addSerialNoColumn) { + return 1; + } + + return 0; + } + + getColumnCount(skipStandardColumns) { + let val = this.columns.length; + + if (skipStandardColumns) { + val = val - this.getStandardColumnCount(); + } + + return val; + } + + getColumn(colIndex) { + colIndex = +colIndex; + return this.columns.find(col => col.colIndex === colIndex); + } + + getRow(rowIndex) { + rowIndex = +rowIndex; + return this.rows.find(row => row[0].rowIndex === rowIndex); + } + + getCell(colIndex, rowIndex) { + rowIndex = +rowIndex; + colIndex = +colIndex; + return this.rows.find(row => row[0].rowIndex === rowIndex)[colIndex]; + } + + get() { + return { + columns: this.columns, + rows: this.rows + }; + } + + hasColumn(name) { + return Boolean(this.columns.find(col => col.content === name)); + } + + hasColumnById(id) { + return Boolean(this.columns.find(col => col.id === id)); + } + + getColumnIndex(name) { + return this.columns.findIndex(col => col.content === name); + } + + getColumnIndexById(id) { + return this.columns.findIndex(col => col.id === id); + } + + getCheckboxHTML() { + return ''; + } +} + +// Custom Errors +class DataError extends TypeError {} + +class ColumnManager { + constructor(instance) { + this.instance = instance; + + linkProperties(this, this.instance, [ + 'options', + 'fireEvent', + 'header', + 'datamanager', + 'style', + 'wrapper', + 'rowmanager', + 'bodyScrollable' + ]); + + this.bindEvents(); + getDropdownHTML = getDropdownHTML.bind(this, this.options.dropdownButton); + } + + renderHeader() { + this.header.innerHTML = ''; + this.refreshHeader(); + } + + refreshHeader() { + const columns = this.datamanager.getColumns(); + + if (!$('.data-table-col', this.header)) { + // insert html + + let html = this.rowmanager.getRowHTML(columns, { + isHeader: 1 + }); + if (this.options.enableInlineFilters) { + html += this.rowmanager.getRowHTML(columns, { + isFilter: 1 + }); + } + + $('thead', this.header).innerHTML = html; + + this.$filterRow = $('.data-table-row[data-is-filter]', this.header); + + if (this.$filterRow) { + // hide filter row immediately, so it doesn't disturb layout + $.style(this.$filterRow, { + display: 'none' + }); + } + } else { + // refresh dom state + const $cols = $.each('.data-table-col', this.header); + if (columns.length < $cols.length) { + // deleted column + $('thead', this.header).innerHTML = this.rowmanager.getRowHTML(columns, { + isHeader: 1 + }); + return; + } + + $cols.map(($col, i) => { + const column = columns[i]; + // column sorted or order changed + // update colIndex of each header cell + $.data($col, { + colIndex: column.colIndex + }); + + // refresh sort indicator + const sortIndicator = $('.sort-indicator', $col); + if (sortIndicator) { + sortIndicator.innerHTML = this.options.sortIndicator[column.sortOrder]; + } + }); + } + // reset columnMap + this.$columnMap = []; + } + + bindEvents() { + this.bindDropdown(); + this.bindResizeColumn(); + this.bindMoveColumn(); + this.bindFilter(); + } + + bindDropdown() { + let $activeDropdown; + $.on(this.header, 'click', '.data-table-dropdown-toggle', (e, $button) => { + const $dropdown = $.closest('.data-table-dropdown', $button); + + if (!$dropdown.classList.contains('is-active')) { + deactivateDropdown(); + $dropdown.classList.add('is-active'); + $activeDropdown = $dropdown; + } else { + deactivateDropdown(); + } + }); + + $.on(document.body, 'click', (e) => { + if (e.target.matches('.data-table-dropdown-toggle')) return; + deactivateDropdown(); + }); + + const dropdownItems = this.options.headerDropdown; + + $.on(this.header, 'click', '.data-table-dropdown-list > div', (e, $item) => { + const $col = $.closest('.data-table-col', $item); + const { + index + } = $.data($item); + const { + colIndex + } = $.data($col); + let callback = dropdownItems[index].action; + + callback && callback.call(this.instance, this.getColumn(colIndex)); + }); + + function deactivateDropdown(e) { + $activeDropdown && $activeDropdown.classList.remove('is-active'); + $activeDropdown = null; + } + } + + bindResizeColumn() { + let isDragging = false; + let $resizingCell, startWidth, startX; + + $.on(this.header, 'mousedown', '.data-table-col .column-resizer', (e, $handle) => { + document.body.classList.add('data-table-resize'); + const $cell = $handle.parentNode.parentNode; + $resizingCell = $cell; + const { + colIndex + } = $.data($resizingCell); + const col = this.getColumn(colIndex); + + if (col && col.resizable === false) { + return; + } + + isDragging = true; + startWidth = $.style($('.content', $resizingCell), 'width'); + startX = e.pageX; + }); + + $.on(document.body, 'mouseup', (e) => { + document.body.classList.remove('data-table-resize'); + if (!$resizingCell) return; + isDragging = false; + + const { + colIndex + } = $.data($resizingCell); + this.setColumnWidth(colIndex); + this.style.setBodyStyle(); + $resizingCell = null; + }); + + $.on(document.body, 'mousemove', (e) => { + if (!isDragging) return; + const finalWidth = startWidth + (e.pageX - startX); + const { + colIndex + } = $.data($resizingCell); + + if (this.getColumnMinWidth(colIndex) > finalWidth) { + // don't resize past minWidth + return; + } + this.datamanager.updateColumn(colIndex, { + width: finalWidth + }); + this.setColumnHeaderWidth(colIndex); + }); + } + + bindMoveColumn() { + let initialized; + + const initialize = () => { + if (initialized) { + $.off(document.body, 'mousemove', initialize); + return; + } + const ready = $('.data-table-col', this.header); + if (!ready) return; + + const $parent = $('.data-table-row', this.header); + + this.sortable = Sortable.create($parent, { + onEnd: (e) => { + const { + oldIndex, + newIndex + } = e; + const $draggedCell = e.item; + const { + colIndex + } = $.data($draggedCell); + if (+colIndex === newIndex) return; + + this.switchColumn(oldIndex, newIndex); + }, + preventOnFilter: false, + filter: '.column-resizer, .data-table-dropdown', + animation: 150 + }); + }; + + $.on(document.body, 'mousemove', initialize); + } + + bindSortColumn() { + + $.on(this.header, 'click', '.data-table-col .column-title', (e, span) => { + const $cell = span.closest('.data-table-col'); + let { + colIndex, + sortOrder + } = $.data($cell); + sortOrder = getDefault(sortOrder, 'none'); + const col = this.getColumn(colIndex); + + if (col && col.sortable === false) { + return; + } + + // reset sort indicator + $('.sort-indicator', this.header).textContent = ''; + $.each('.data-table-col', this.header).map($cell => { + $.data($cell, { + sortOrder: 'none' + }); + }); + + let nextSortOrder, textContent; + if (sortOrder === 'none') { + nextSortOrder = 'asc'; + textContent = '▲'; + } else if (sortOrder === 'asc') { + nextSortOrder = 'desc'; + textContent = '▼'; + } else if (sortOrder === 'desc') { + nextSortOrder = 'none'; + textContent = ''; + } + + $.data($cell, { + sortOrder: nextSortOrder + }); + $('.sort-indicator', $cell).textContent = textContent; + + this.sortColumn(colIndex, nextSortOrder); + }); + } + + sortColumn(colIndex, nextSortOrder) { + this.instance.freeze(); + this.sortRows(colIndex, nextSortOrder) + .then(() => { + this.refreshHeader(); + return this.rowmanager.refreshRows(); + }) + .then(() => this.instance.unfreeze()) + .then(() => { + this.fireEvent('onSortColumn', this.getColumn(colIndex)); + }); + } + + removeColumn(colIndex) { + const removedCol = this.getColumn(colIndex); + this.instance.freeze(); + this.datamanager.removeColumn(colIndex) + .then(() => { + this.refreshHeader(); + return this.rowmanager.refreshRows(); + }) + .then(() => this.instance.unfreeze()) + .then(() => { + this.fireEvent('onRemoveColumn', removedCol); + }); + } + + switchColumn(oldIndex, newIndex) { + this.instance.freeze(); + this.datamanager.switchColumn(oldIndex, newIndex) + .then(() => { + this.refreshHeader(); + return this.rowmanager.refreshRows(); + }) + .then(() => { + this.setColumnWidth(oldIndex); + this.setColumnWidth(newIndex); + this.instance.unfreeze(); + }) + .then(() => { + this.fireEvent('onSwitchColumn', + this.getColumn(oldIndex), this.getColumn(newIndex) + ); + }); + } + + toggleFilter() { + this.isFilterShown = this.isFilterShown || false; + + if (this.isFilterShown) { + $.style(this.$filterRow, { + display: 'none' + }); + } else { + $.style(this.$filterRow, { + display: '' + }); + } + + this.isFilterShown = !this.isFilterShown; + this.style.setBodyStyle(); + } + + focusFilter(colIndex) { + if (!this.isFilterShown) return; + + const $filterInput = $(`[data-col-index="${colIndex}"] .data-table-filter`, this.$filterRow); + $filterInput.focus(); + } + + bindFilter() { + if (!this.options.enableInlineFilters) return; + const handler = e => { + const $filterCell = $.closest('.data-table-col', e.target); + const { + colIndex + } = $.data($filterCell); + const keyword = e.target.value; + + this.datamanager.filterRows(keyword, colIndex) + .then(({ + rowsToHide, + rowsToShow + }) => { + rowsToHide.map(rowIndex => { + const $tr = $(`.data-table-row[data-row-index="${rowIndex}"]`, this.bodyScrollable); + $tr.classList.add('hide'); + }); + rowsToShow.map(rowIndex => { + const $tr = $(`.data-table-row[data-row-index="${rowIndex}"]`, this.bodyScrollable); + $tr.classList.remove('hide'); + }); + }); + }; + $.on(this.header, 'keydown', '.data-table-filter', debounce$2(handler, 300)); + } + + sortRows(colIndex, sortOrder) { + return this.datamanager.sortRows(colIndex, sortOrder); + } + + getColumn(colIndex) { + return this.datamanager.getColumn(colIndex); + } + + getColumns() { + return this.datamanager.getColumns(); + } + + setColumnWidth(colIndex) { + colIndex = +colIndex; + this._columnWidthMap = this._columnWidthMap || []; + + const { + width + } = this.getColumn(colIndex); + + let index = this._columnWidthMap[colIndex]; + const selector = `[data-col-index="${colIndex}"] .content, [data-col-index="${colIndex}"] .edit-cell`; + const styles = { + width: width + 'px' + }; + + index = this.style.setStyle(selector, styles, index); + this._columnWidthMap[colIndex] = index; + } + + setColumnHeaderWidth(colIndex) { + colIndex = +colIndex; + this.$columnMap = this.$columnMap || []; + const selector = `.data-table-header [data-col-index="${colIndex}"] .content`; + const { + width + } = this.getColumn(colIndex); + + let $column = this.$columnMap[colIndex]; + if (!$column) { + $column = this.header.querySelector(selector); + this.$columnMap[colIndex] = $column; + } + + $column.style.width = width + 'px'; + } + + getColumnMinWidth(colIndex) { + colIndex = +colIndex; + return this.getColumn(colIndex).minWidth || 24; + } + + getFirstColumnIndex() { + if (this.options.addCheckboxColumn && this.options.addSerialNoColumn) { + return 2; + } + + if (this.options.addCheckboxColumn || this.options.addSerialNoColumn) { + return 1; + } + + return 0; + } + + getHeaderCell$(colIndex) { + return $(`.data-table-col[data-col-index="${colIndex}"]`, this.header); + } + + getLastColumnIndex() { + return this.datamanager.getColumnCount() - 1; + } + + getSerialColumnIndex() { + const columns = this.datamanager.getColumns(); + + return columns.findIndex(column => column.content.includes('Sr. No')); + } +} + +// eslint-disable-next-line +var getDropdownHTML = function getDropdownHTML(dropdownButton = 'v') { + // add dropdown buttons + const dropdownItems = this.options.headerDropdown; + + return `
${dropdownButton}
+
+ ${dropdownItems.map((d, i) => `
${d.label}
`).join('')} +
+ `; +}; + +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; + + this.bindEvents(); + } + + bindEvents() { + this.bindFocusCell(); + this.bindEditCell(); + this.bindKeyboardSelection(); + this.bindCopyCellContents(); + this.bindMouseEvents(); + } + + bindFocusCell() { + this.bindKeyboardNav(); + } + + bindEditCell() { + this.$editingCell = null; + + $.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.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); + $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 ` +
+ ${(contentHTML)} + ${sortIndicator} + ${resizeColumn} + ${dropdown} +
+ ${editCellHTML} + `; + } + + getEditCellHTML() { + return ` +
+ `; + } + + 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; + + 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 []; + } + + return this.checkMap + .map((c, rowIndex) => { + if (c) { + return rowIndex; + } + 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; + 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); + } + + 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; + } + + 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 = ` + + ${this.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 = ` + + ${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 { + constructor(instance) { + this.instance = instance; + + linkProperties(this, this.instance, [ + 'options', 'datamanager', 'columnmanager', + 'header', 'bodyScrollable', 'getColumn' + ]); + + this.scopeClass = 'datatable-instance-' + instance.constructor.instances; + instance.datatableWrapper.classList.add(this.scopeClass); + + const styleEl = document.createElement('style'); + instance.wrapper.insertBefore(styleEl, instance.datatableWrapper); + this.styleEl = styleEl; + this.styleSheet = styleEl.sheet; + + this.bindResizeWindow(); + } + + bindResizeWindow() { + if (this.options.layout === 'fluid') { + $.on(window, 'resize', throttle$1(() => { + this.distributeRemainingWidth(); + this.refreshColumnWidth(); + this.setBodyStyle(); + }, 300)); + } + } + + destroy() { + this.styleEl.remove(); + } + + setStyle(selector, styleMap, index = -1) { + const styles = Object.keys(styleMap) + .map(prop => { + if (!prop.includes('-')) { + prop = camelCaseToDash(prop); + } + return `${prop}:${styleMap[prop]};`; + }) + .join(''); + let prefixedSelector = selector + .split(',') + .map(r => `.${this.scopeClass} ${r}`) + .join(','); + + let ruleString = `${prefixedSelector} { ${styles} }`; + + let _index = this.styleSheet.cssRules.length; + if (index !== -1) { + this.styleSheet.deleteRule(index); + _index = index; + } + + this.styleSheet.insertRule(ruleString, _index); + return _index; + } + + setDimensions() { + this.setHeaderStyle(); + + this.setupMinWidth(); + this.setupNaturalColumnWidth(); + this.setupColumnWidth(); + + this.distributeRemainingWidth(); + this.setColumnStyle(); + this.setDefaultCellHeight(); + this.setBodyStyle(); + } + + setHeaderStyle() { + if (this.options.layout === 'fluid') { + // setting width as 0 will ensure that the + // header doesn't take the available space + $.style(this.header, { + width: 0 + }); + } + + $.style(this.header, { + margin: 0 + }); + + // don't show resize cursor on nonResizable columns + const nonResizableColumnsSelector = this.datamanager.getColumns() + .filter(col => col.resizable === false) + .map(col => col.colIndex) + .map(i => `.data-table-header [data-col-index="${i}"]`) + .join(); + + this.setStyle(nonResizableColumnsSelector, { + cursor: 'pointer' + }); + } + + setupMinWidth() { + $.each('.data-table-col[data-is-header]', this.header).map(col => { + const width = $.style($('.content', col), 'width'); + const { + colIndex + } = $.data(col); + const column = this.getColumn(colIndex); + + if (!column.minWidth) { + // only set this once + column.minWidth = width; + } + }); + } + + setupNaturalColumnWidth() { + if (!$('.data-table-row')) return; + + // set initial width as naturally calculated by table's first row + $.each('.data-table-row[data-row-index="0"] .data-table-col', this.bodyScrollable).map($cell => { + const { + colIndex + } = $.data($cell); + const column = this.datamanager.getColumn(colIndex); + + let naturalWidth = $.style($('.content', $cell), 'width'); + + if (column.id === '_rowIndex') { + // width based on rowCount + const rowCount = this.datamanager.getRowCount(); + const digits = (rowCount + '').length; + if (digits > 1) { + naturalWidth = naturalWidth + ((digits - 1) * 8); + } + } + + column.naturalWidth = naturalWidth; + }); + } + + setupColumnWidth() { + this.datamanager.getColumns() + .map(column => { + if (!column.width) { + column.width = column.naturalWidth; + } + if (column.width < column.minWidth) { + column.width = column.minWidth; + } + }); + } + + distributeRemainingWidth() { + if (this.options.layout !== 'fluid') return; + + const wrapperWidth = $.style(this.instance.datatableWrapper, 'width'); + const headerWidth = $.style(this.header, 'width'); + const resizableColumns = this.datamanager.getColumns().filter(col => col.resizable); + const deltaWidth = (wrapperWidth - headerWidth) / resizableColumns.length; + + resizableColumns.map(col => { + const width = $.style(this.getColumnHeaderElement(col.colIndex), 'width'); + let finalWidth = Math.floor(width + deltaWidth) - 2; + + this.datamanager.updateColumn(col.colIndex, { + width: finalWidth + }); + }); + } + + setDefaultCellHeight() { + if (this.__cellHeightSet) return; + const height = this.options.cellHeight || + $.style($('.data-table-col', this.instance.datatableWrapper), 'height'); + if (height) { + this.setCellHeight(height); + this.__cellHeightSet = true; + } + } + + setCellHeight(height) { + this.setStyle('.data-table-col .content', { + height: height + 'px' + }); + this.setStyle('.data-table-col .edit-cell', { + height: height + 'px' + }); + } + + setColumnStyle() { + // align columns + this.datamanager.getColumns() + .map(column => { + // alignment + if (['left', 'center', 'right'].includes(column.align)) { + this.setStyle(`[data-col-index="${column.colIndex}"]`, { + 'text-align': column.align + }); + } + // width + this.columnmanager.setColumnHeaderWidth(column.colIndex); + this.columnmanager.setColumnWidth(column.colIndex); + }); + this.setBodyStyle(); + } + + refreshColumnWidth() { + this.datamanager.getColumns() + .map(column => { + this.columnmanager.setColumnHeaderWidth(column.colIndex); + this.columnmanager.setColumnWidth(column.colIndex); + }); + } + + setBodyStyle() { + const width = $.style(this.header, 'width'); + + $.style(this.bodyScrollable, { + width: width + 'px' + }); + + $.style(this.bodyScrollable, { + marginTop: $.style(this.header, 'height') + 'px' + }); + + $.style($('table', this.bodyScrollable), { + margin: 0 + }); + } + + getColumnHeaderElement(colIndex) { + colIndex = +colIndex; + if (colIndex < 0) return null; + return $(`.data-table-col[data-col-index="${colIndex}"]`, this.header); + } +} + +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' +}; + +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; + } + + 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()); + + 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'); + } + }, + { + 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) {} + }, + 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 +}; + +class DataTable { + 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`'); + } + + 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(); + } + + prepareDom() { + this.wrapper.innerHTML = ` +
+ +
+
+
+
+ ${this.options.freezeMessage} +
+ +
+ `; + + 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'); + } + + 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; + +var name = "frappe-datatable"; +var version = "0.0.2"; +var description = "A modern datatable library for the web"; +var main = "dist/frappe-datatable.cjs.js"; +var scripts = {"start":"npm run dev","build":"rollup -c","dev":"rollup -c -w","test":"mocha --compilers js:babel-core/register --colors ./test/*.spec.js","test:watch":"mocha --compilers js:babel-core/register --colors -w ./test/*.spec.js"}; +var devDependencies = {"chai":"3.5.0","cssnano":"^3.10.0","deepmerge":"^2.0.1","eslint":"3.19.0","eslint-loader":"1.7.1","mocha":"3.3.0","postcss-cssnext":"^3.1.0","postcss-nested":"^3.0.0","precss":"^3.1.0","rollup-plugin-commonjs":"^8.3.0","rollup-plugin-json":"^2.3.0","rollup-plugin-node-resolve":"^3.0.3","rollup-plugin-postcss":"^1.2.8","rollup-plugin-uglify":"^3.0.0"}; +var repository = {"type":"git","url":"https://github.com/frappe/datatable.git"}; +var keywords = ["datatable","data","grid","table"]; +var author = "Faris Ansari"; +var license = "MIT"; +var bugs = {"url":"https://github.com/frappe/datatable/issues"}; +var homepage = "https://frappe.github.io/datatable"; +var dependencies = {"clusterize.js":"^0.18.0","lodash":"^4.17.5","sortablejs":"^1.7.0"}; +var packageJson = { + name: name, + version: version, + description: description, + main: main, + scripts: scripts, + devDependencies: devDependencies, + repository: repository, + keywords: keywords, + author: author, + license: license, + bugs: bugs, + homepage: homepage, + dependencies: dependencies +}; + +DataTable.__version__ = packageJson.version; + +return DataTable; + +}(Sortable,Clusterize)); diff --git a/docs/index.css b/docs/index.css new file mode 100644 index 0000000..786b20d --- /dev/null +++ b/docs/index.css @@ -0,0 +1,40 @@ +*, *::after, *::before { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html, body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-size: 14px; +} + +h1 { + font-size: 3rem; + margin: 1rem 0; +} + +h2 { + font-size: 2.5rem; +} + +.showcase { + margin: 0 auto; + padding: 4rem; + text-align: center; +} + +.showcase p { + font-size: 2rem; + margin: 1rem 0; +} + +.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 new file mode 100644 index 0000000..aef8ca7 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,27 @@ + + + + + + + Frappe DataTable - A simple, modern datatable library for the web + + + + +
+

Frappe DataTable

+

A simple, modern and interactive datatable for the web

+
+
+
+

Tree Structure

+

Show tree structured rows in your table

+
+
+ + + + + + \ No newline at end of file diff --git a/docs/index.js b/docs/index.js new file mode 100644 index 0000000..372b4af --- /dev/null +++ b/docs/index.js @@ -0,0 +1,691 @@ +/* global DataTable */ +/* eslint-disable no-unused-vars */ + +const { + columns, + data +} = getSampleData(); + +let datatable1 = new DataTable('.datatable-1', { + columns, + data +}); + +let datatable2 = new DataTable('.datatable-2', Object.assign(getTreeData(), { + enableInlineFilters: true +})); + +function getSampleData(multiplier) { + let columns = ['Name', 'Position', 'Office', 'Extn.', 'Start Date', 'Salary']; + let data = [ + ['Tiger Nixon', 'System Architect', 'Edinburgh', '5421', '2011/04/25', '$320,800'], + ['Garrett Winters', 'Accountant', 'Tokyo', '8422', '2011/07/25', '$170,750'], + ['Ashton Cox', 'Junior Technical Author', 'San Francisco', '1562', '2009/01/12', '$86,000'], + ['Cedric Kelly', 'Senior Javascript Developer', 'Edinburgh', '6224', '2012/03/29', '$433,060'], + ['Airi Satou', 'Accountant', 'Tokyo', '5407', '2008/11/28', '$162,700'], + ['Brielle Williamson', 'Integration Specialist', 'New York', '4804', '2012/12/02', '$372,000'], + ['Herrod Chandler', 'Sales Assistant', 'San Francisco', '9608', '2012/08/06', '$137,500'], + ['Rhona Davidson', 'Integration Specialist', 'Tokyo', '6200', '2010/10/14', '$327,900'], + ['Colleen Hurst', 'Javascript Developer', 'San Francisco', '2360', '2009/09/15', '$205,500'], + ['Sonya Frost', 'Software Engineer', 'Edinburgh', '1667', '2008/12/13', '$103,600'], + ['Jena Gaines', 'Office Manager', 'London', '3814', '2008/12/19', '$90,560'], + ['Quinn Flynn', 'Support Lead', 'Edinburgh', '9497', '2013/03/03', '$342,000'], + ['Charde Marshall', 'Regional Director', 'San Francisco', '6741', '2008/10/16', '$470,600'], + ['Haley Kennedy', 'Senior Marketing Designer', 'London', '3597', '2012/12/18', '$313,500'], + ['Tatyana Fitzpatrick', 'Regional Director', 'London', '1965', '2010/03/17', '$385,750'], + ['Michael Silva', 'Marketing Designer', 'London', '1581', '2012/11/27', '$198,500'], + ['Paul Byrd', 'Chief Financial Officer (CFO)', 'New York', '3059', '2010/06/09', '$725,000'], + ['Gloria Little', 'Systems Administrator', 'New York', '1721', '2009/04/10', '$237,500'], + ['Bradley Greer', 'Software Engineer', 'London', '2558', '2012/10/13', '$132,000'], + ['Dai Rios', 'Personnel Lead', 'Edinburgh', '2290', '2012/09/26', '$217,500'], + ['Jenette Caldwell', 'Development Lead', 'New York', '1937', '2011/09/03', '$345,000'], + ['Yuri Berry', 'Chief Marketing Officer (CMO)', 'New York', '6154', '2009/06/25', '$675,000'], + ['Caesar Vance', 'Pre-Sales Support', 'New York', '8330', '2011/12/12', '$106,450'], + ['Doris Wilder', 'Sales Assistant', 'Sidney', '3023', '2010/09/20', '$85,600'], + ['Angelica Ramos', 'Chief Executive Officer (CEO)', 'London', '5797', '2009/10/09', '$1,200,000'], + ['Gavin Joyce', 'Developer', 'Edinburgh', '8822', '2010/12/22', '$92,575'], + ['Jennifer Chang', 'Regional Director', 'Singapore', '9239', '2010/11/14', '$357,650'], + ['Brenden Wagner', 'Software Engineer', 'San Francisco', '1314', '2011/06/07', '$206,850'], + ['Fiona Green', 'Chief Operating Officer (COO)', 'San Francisco', '2947', '2010/03/11', '$850,000'], + ['Shou Itou', 'Regional Marketing', 'Tokyo', '8899', '2011/08/14', '$163,000'], + ['Michelle House', 'Integration Specialist', 'Sidney', '2769', '2011/06/02', '$95,400'], + ['Suki Burks', 'Developer', 'London', '6832', '2009/10/22', '$114,500'], + ['Prescott Bartlett', 'Technical Author', 'London', '3606', '2011/05/07', '$145,000'], + ['Gavin Cortez', 'Team Leader', 'San Francisco', '2860', '2008/10/26', '$235,500'], + ['Martena Mccray', 'Post-Sales support', 'Edinburgh', '8240', '2011/03/09', '$324,050'], + ['Unity Butler', 'Marketing Designer', 'San Francisco', '5384', '2009/12/09', '$85,675'], + ['Howard Hatfield', 'Office Manager', 'San Francisco', '7031', '2008/12/16', '$164,500'], + ['Hope Fuentes', 'Secretary', 'San Francisco', '6318', '2010/02/12', '$109,850'], + ['Vivian Harrell', 'Financial Controller', 'San Francisco', '9422', '2009/02/14', '$452,500'], + ['Timothy Mooney', 'Office Manager', 'London', '7580', '2008/12/11', '$136,200'], + ['Jackson Bradshaw', 'Director', 'New York', '1042', '2008/09/26', '$645,750'], + ['Olivia Liang', 'Support Engineer', 'Singapore', '2120', '2011/02/03', '$234,500'], + ['Bruno Nash', 'Software Engineer', 'London', '6222', '2011/05/03', '$163,500'], + ['Sakura Yamamoto', 'Support Engineer', 'Tokyo', '9383', '2009/08/19', '$139,575'], + ['Thor Walton', 'Developer', 'New York', '8327', '2013/08/11', '$98,540'], + ['Finn Camacho', 'Support Engineer', 'San Francisco', '2927', '2009/07/07', '$87,500'], + ['Serge Baldwin', 'Data Coordinator', 'Singapore', '8352', '2012/04/09', '$138,575'], + ['Zenaida Frank', 'Software Engineer', 'New York', '7439', '2010/01/04', '$125,250'], + ['Zorita Serrano', 'Software Engineer', 'San Francisco', '4389', '2012/06/01', '$115,000'], + ['Jennifer Acosta', 'Junior Javascript Developer', 'Edinburgh', '3431', '2013/02/01', '$75,650'], + ['Cara Stevens', 'Sales Assistant', 'New York', '3990', '2011/12/06', '$145,600'], + ['Hermione Butler', 'Regional Director', 'London', '1016', '2011/03/21', '$356,250'], + ['Lael Greer', 'Systems Administrator', 'London', '6733', '2009/02/27', '$103,500'], + ['Jonas Alexander', 'Developer', 'San Francisco', '8196', '2010/07/14', '$86,500'], + ['Shad Decker', 'Regional Director', 'Edinburgh', '6373', '2008/11/13', '$183,000'], + ['Michael Bruce', 'Javascript Developer', 'Singapore', '5384', '2011/06/27', '$183,000'], + ['Donna Snider', 'Customer Support', 'New York', '4226', '2011/01/25', '$112,000'] + ]; + + if (multiplier) { + Array.from(new Array(multiplier - 1)).forEach(d => { + data = data.concat(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' + }] + }; +} diff --git a/rollup.config.js b/rollup.config.js index 5aacb6b..4e618b3 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -8,34 +8,45 @@ import cssnext from 'postcss-cssnext'; // import cssnano from 'cssnano'; const dev = { - input: 'src/index.js', - output: { - file: 'dist/frappe-datatable.js', - format: 'iife', - name: 'DataTable', - globals: { - sortablejs: 'Sortable', - 'clusterize.js': 'Clusterize' - } - }, - plugins: [ - json(), - nodeResolve(), - commonjs(), - postcss({ - extract: 'dist/frappe-datatable.css', - plugins: [ - nested(), - cssnext() - ] - }) - ], - external: ['sortablejs', 'clusterize.js'] + input: 'src/index.js', + output: [{ + file: 'dist/frappe-datatable.js', + format: 'iife', + name: 'DataTable', + globals: { + sortablejs: 'Sortable', + 'clusterize.js': 'Clusterize' + } + }, { + file: 'docs/frappe-datatable.js', + format: 'iife', + name: 'DataTable', + globals: { + sortablejs: 'Sortable', + 'clusterize.js': 'Clusterize' + } + }], + plugins: [ + json(), + nodeResolve(), + commonjs(), + postcss({ + extract: ['dist/frappe-datatable.css', 'docs/frappe-datatable.css'], + plugins: [ + nested(), + cssnext() + ] + }) + ], + external: ['sortablejs', 'clusterize.js'] }; -export default [dev, Object.assign({}, dev, { - output: { - format: 'cjs', - file: 'dist/frappe-datatable.cjs.js' - } -})]; +export default [ + dev, + Object.assign({}, dev, { + output: { + format: 'cjs', + file: 'dist/frappe-datatable.cjs.js' + } + }) +]; diff --git a/src/body-renderer.js b/src/body-renderer.js index 5c1cb5e..17e8435 100644 --- a/src/body-renderer.js +++ b/src/body-renderer.js @@ -1,99 +1,101 @@ import $ from './dom'; import Clusterize from 'clusterize.js'; -import { promisify } from './utils'; +import { + promisify +} from './utils'; export default 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); + + 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('')} + + `; + } }; - -export function getBodyHTML(rows) { - return ` - - ${rows.map(row => this.rowmanager.getRowHTML(row, { rowIndex: row[0].rowIndex })).join('')} - - `; -} diff --git a/src/cellmanager.js b/src/cellmanager.js index 12645e2..b8ea6c3 100644 --- a/src/cellmanager.js +++ b/src/cellmanager.js @@ -1,609 +1,657 @@ import { - copyTextToClipboard, - makeDataAttributeString, - throttle + copyTextToClipboard, + makeDataAttributeString, + throttle } from './utils'; import $ from './dom'; -import { getDropdownHTML } from './columnmanager'; +import { + getDropdownHTML +} from './columnmanager'; export default 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(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(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} @@ -612,15 +660,15 @@ export default 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}"]`; + } } diff --git a/src/columnmanager.js b/src/columnmanager.js index e67951d..6f4191a 100644 --- a/src/columnmanager.js +++ b/src/columnmanager.js @@ -1,413 +1,452 @@ import $ from './dom'; import Sortable from 'sortablejs'; -import { getDefault, linkProperties, debounce } from './utils'; +import { + getDefault, + linkProperties, + debounce +} from './utils'; export default 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(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(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('')}
@@ -415,5 +454,5 @@ var getDropdownHTML = function getDropdownHTML(dropdownButton = 'v') { }; export { - getDropdownHTML + getDropdownHTML }; diff --git a/src/datamanager.js b/src/datamanager.js index 4c6993e..3569118 100644 --- a/src/datamanager.js +++ b/src/datamanager.js @@ -1,500 +1,517 @@ -import { isNumeric, promisify } from './utils'; +import { + isNumeric, + promisify +} from './utils'; export default 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.forEach((row, index) => { + row.forEach(cell => { + if (cell.colIndex === srNoColIndex) { + cell.content = (index + 1) + ''; + } + }); + }); + } + } - 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 diff --git a/src/datatable.js b/src/datatable.js index 89d6aec..33bad20 100644 --- a/src/datatable.js +++ b/src/datatable.js @@ -10,50 +10,48 @@ import DEFAULT_OPTIONS from './defaults'; import './style.css'; 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 = `
@@ -67,110 +65,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/src/defaults.js b/src/defaults.js index f306723..30f228e 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -1,51 +1,51 @@ export default { - 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 }; diff --git a/src/dom.js b/src/dom.js index 29b2cf5..f930a90 100644 --- a/src/dom.js +++ b/src/dom.js @@ -1,172 +1,181 @@ - export default 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; + }); }; diff --git a/src/keyboard.js b/src/keyboard.js index abd9e96..77f2cbe 100644 --- a/src/keyboard.js +++ b/src/keyboard.js @@ -1,56 +1,56 @@ import $ from './dom'; 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' }; export default 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); + }); + } } diff --git a/src/performance.js b/src/performance.js deleted file mode 100644 index 9dcd0de..0000000 --- a/src/performance.js +++ /dev/null @@ -1,14 +0,0 @@ -class Performance { - start() { - this._start = window.performance.now(); - } - - end() { - this._end = window.performance.now(); - console.log(this._end - this._start); - } -} - -let perf = new Performance(); - -export default perf; diff --git a/src/rowmanager.js b/src/rowmanager.js index f601045..28fb68d 100644 --- a/src/rowmanager.js +++ b/src/rowmanager.js @@ -1,210 +1,224 @@ import $ from './dom'; -import { makeDataAttributeString, promisify } from './utils'; +import { + makeDataAttributeString, + promisify +} from './utils'; export default 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 ``; + } } diff --git a/src/style.css b/src/style.css index 9ed0c29..9cd2d1a 100644 --- a/src/style.css +++ b/src/style.css @@ -1,3 +1,4 @@ +/* This file is processed by postcss */ /* variables */ :root { diff --git a/src/style.js b/src/style.js index 714eb49..0e29d32 100644 --- a/src/style.js +++ b/src/style.js @@ -1,235 +1,241 @@ import $ from './dom'; import { - camelCaseToDash, - linkProperties, - throttle + camelCaseToDash, + linkProperties, + throttle } from './utils'; export default class Style { - constructor(instance) { - this.instance = instance; + constructor(instance) { + this.instance = instance; - linkProperties(this, this.instance, [ - 'options', 'datamanager', 'columnmanager', - 'header', 'bodyScrollable', 'getColumn' - ]); + linkProperties(this, this.instance, [ + 'options', 'datamanager', 'columnmanager', + 'header', 'bodyScrollable', 'getColumn' + ]); - this.scopeClass = 'datatable-instance-' + instance.constructor.instances; - instance.datatableWrapper.classList.add(this.scopeClass); + this.scopeClass = 'datatable-instance-' + instance.constructor.instances; + instance.datatableWrapper.classList.add(this.scopeClass); - const styleEl = document.createElement('style'); - instance.wrapper.insertBefore(styleEl, instance.datatableWrapper); - this.styleEl = styleEl; - this.styleSheet = styleEl.sheet; + const styleEl = document.createElement('style'); + instance.wrapper.insertBefore(styleEl, instance.datatableWrapper); + this.styleEl = styleEl; + this.styleSheet = styleEl.sheet; - this.bindResizeWindow(); - } + this.bindResizeWindow(); + } + + bindResizeWindow() { + if (this.options.layout === 'fluid') { + $.on(window, 'resize', throttle(() => { + this.distributeRemainingWidth(); + this.refreshColumnWidth(); + this.setBodyStyle(); + }, 300)); + } + } + + destroy() { + this.styleEl.remove(); + } + + setStyle(selector, styleMap, index = -1) { + const styles = Object.keys(styleMap) + .map(prop => { + if (!prop.includes('-')) { + prop = camelCaseToDash(prop); + } + return `${prop}:${styleMap[prop]};`; + }) + .join(''); + let prefixedSelector = selector + .split(',') + .map(r => `.${this.scopeClass} ${r}`) + .join(','); + + let ruleString = `${prefixedSelector} { ${styles} }`; + + let _index = this.styleSheet.cssRules.length; + if (index !== -1) { + this.styleSheet.deleteRule(index); + _index = index; + } + + this.styleSheet.insertRule(ruleString, _index); + return _index; + } + + setDimensions() { + this.setHeaderStyle(); + + this.setupMinWidth(); + this.setupNaturalColumnWidth(); + this.setupColumnWidth(); - bindResizeWindow() { - if (this.options.layout === 'fluid') { - $.on(window, 'resize', throttle(() => { this.distributeRemainingWidth(); - this.refreshColumnWidth(); + this.setColumnStyle(); + this.setDefaultCellHeight(); this.setBodyStyle(); - }, 300)); - } - } - - destroy() { - this.styleEl.remove(); - } - - setStyle(rule, styleMap, index = -1) { - const styles = Object.keys(styleMap) - .map(prop => { - if (!prop.includes('-')) { - prop = camelCaseToDash(prop); - } - return `${prop}:${styleMap[prop]};`; - }) - .join(''); - let ruleString = `.${this.scopeClass} ${rule} { ${styles} }`; - - let _index = this.styleSheet.cssRules.length; - if (index !== -1) { - this.styleSheet.deleteRule(index); - _index = index; } - this.styleSheet.insertRule(ruleString, _index); - return _index; - } + setHeaderStyle() { + if (this.options.layout === 'fluid') { + // setting width as 0 will ensure that the + // header doesn't take the available space + $.style(this.header, { + width: 0 + }); + } - setDimensions() { - this.setHeaderStyle(); + $.style(this.header, { + margin: 0 + }); - this.setupMinWidth(); - this.setupNaturalColumnWidth(); - this.setupColumnWidth(); + // don't show resize cursor on nonResizable columns + const nonResizableColumnsSelector = this.datamanager.getColumns() + .filter(col => col.resizable === false) + .map(col => col.colIndex) + .map(i => `.data-table-header [data-col-index="${i}"]`) + .join(); - this.distributeRemainingWidth(); - this.setColumnStyle(); - this.setDefaultCellHeight(); - this.setBodyStyle(); - } - - setHeaderStyle() { - if (this.options.layout === 'fluid') { - // setting width as 0 will ensure that the - // header doesn't take the available space - $.style(this.header, { - width: 0 - }); + this.setStyle(nonResizableColumnsSelector, { + cursor: 'pointer' + }); } - $.style(this.header, { - margin: 0 - }); + setupMinWidth() { + $.each('.data-table-col[data-is-header]', this.header).map(col => { + const width = $.style($('.content', col), 'width'); + const { + colIndex + } = $.data(col); + const column = this.getColumn(colIndex); - // don't show resize cursor on nonResizable columns - const nonResizableColumnsSelector = this.datamanager.getColumns() - .filter(col => col.resizable === false) - .map(col => col.colIndex) - .map(i => `.data-table-header [data-col-index="${i}"]`) - .join(); - - this.setStyle(nonResizableColumnsSelector, { - cursor: 'pointer' - }); - } - - setupMinWidth() { - $.each('.data-table-col[data-is-header]', this.header).map(col => { - const width = $.style($('.content', col), 'width'); - const { - colIndex - } = $.data(col); - const column = this.getColumn(colIndex); - - if (!column.minWidth) { - // only set this once - column.minWidth = width; - } - }); - } - - setupNaturalColumnWidth() { - if (!$('.data-table-row')) return; - - // set initial width as naturally calculated by table's first row - $.each('.data-table-row[data-row-index="0"] .data-table-col', this.bodyScrollable).map($cell => { - const { - colIndex - } = $.data($cell); - const column = this.datamanager.getColumn(colIndex); - - let naturalWidth = $.style($('.content', $cell), 'width'); - - if (column.id === '_rowIndex') { - // width based on rowCount - const rowCount = this.datamanager.getRowCount(); - const digits = (rowCount + '').length; - if (digits > 2) { - naturalWidth = naturalWidth + ((digits - 2) * 8); - } - } - - column.naturalWidth = naturalWidth; - }); - } - - setupColumnWidth() { - this.datamanager.getColumns() - .map(column => { - if (!column.width) { - column.width = column.naturalWidth; - } - if (column.width < column.minWidth) { - column.width = column.minWidth; - } - }); - } - - distributeRemainingWidth() { - if (this.options.layout !== 'fluid') return; - - const wrapperWidth = $.style(this.instance.datatableWrapper, 'width'); - const headerWidth = $.style(this.header, 'width'); - const resizableColumns = this.datamanager.getColumns().filter(col => col.resizable); - const deltaWidth = (wrapperWidth - headerWidth) / resizableColumns.length; - - resizableColumns.map(col => { - const width = $.style(this.getColumnHeaderElement(col.colIndex), 'width'); - let finalWidth = Math.floor(width + deltaWidth) - 2; - - this.datamanager.updateColumn(col.colIndex, { - width: finalWidth - }); - }); - } - - setDefaultCellHeight() { - if (this.__cellHeightSet) return; - const height = this.options.cellHeight || $.style($('.data-table-col', this.instance.datatableWrapper), 'height'); - if (height) { - this.setCellHeight(height); - this.__cellHeightSet = true; + if (!column.minWidth) { + // only set this once + column.minWidth = width; + } + }); } - } - setCellHeight(height) { - this.setStyle('.data-table-col .content', { - height: height + 'px' - }); - this.setStyle('.data-table-col .edit-cell', { - height: height + 'px' - }); - } + setupNaturalColumnWidth() { + if (!$('.data-table-row')) return; - setColumnStyle() { - // align columns - this.datamanager.getColumns() - .map(column => { - // alignment - if (['left', 'center', 'right'].includes(column.align)) { - this.setStyle(`[data-col-index="${column.colIndex}"]`, { - 'text-align': column.align - }); + // set initial width as naturally calculated by table's first row + $.each('.data-table-row[data-row-index="0"] .data-table-col', this.bodyScrollable).map($cell => { + const { + colIndex + } = $.data($cell); + const column = this.datamanager.getColumn(colIndex); + + let naturalWidth = $.style($('.content', $cell), 'width'); + + if (column.id === '_rowIndex') { + // width based on rowCount + const rowCount = this.datamanager.getRowCount(); + const digits = (rowCount + '').length; + if (digits > 1) { + naturalWidth = naturalWidth + ((digits - 1) * 8); + } + } + + column.naturalWidth = naturalWidth; + }); + } + + setupColumnWidth() { + this.datamanager.getColumns() + .map(column => { + if (!column.width) { + column.width = column.naturalWidth; + } + if (column.width < column.minWidth) { + column.width = column.minWidth; + } + }); + } + + distributeRemainingWidth() { + if (this.options.layout !== 'fluid') return; + + const wrapperWidth = $.style(this.instance.datatableWrapper, 'width'); + const headerWidth = $.style(this.header, 'width'); + const resizableColumns = this.datamanager.getColumns().filter(col => col.resizable); + const deltaWidth = (wrapperWidth - headerWidth) / resizableColumns.length; + + resizableColumns.map(col => { + const width = $.style(this.getColumnHeaderElement(col.colIndex), 'width'); + let finalWidth = Math.floor(width + deltaWidth) - 2; + + this.datamanager.updateColumn(col.colIndex, { + width: finalWidth + }); + }); + } + + setDefaultCellHeight() { + if (this.__cellHeightSet) return; + const height = this.options.cellHeight || + $.style($('.data-table-col', this.instance.datatableWrapper), 'height'); + if (height) { + this.setCellHeight(height); + this.__cellHeightSet = true; } - // width - this.columnmanager.setColumnHeaderWidth(column.colIndex); - this.columnmanager.setColumnWidth(column.colIndex); - }); - this.setBodyStyle(); - } + } - refreshColumnWidth() { - this.datamanager.getColumns() - .map(column => { - this.columnmanager.setColumnHeaderWidth(column.colIndex); - this.columnmanager.setColumnWidth(column.colIndex); - }); - } + setCellHeight(height) { + this.setStyle('.data-table-col .content', { + height: height + 'px' + }); + this.setStyle('.data-table-col .edit-cell', { + height: height + 'px' + }); + } - setBodyStyle() { - const width = $.style(this.header, 'width'); + setColumnStyle() { + // align columns + this.datamanager.getColumns() + .map(column => { + // alignment + if (['left', 'center', 'right'].includes(column.align)) { + this.setStyle(`[data-col-index="${column.colIndex}"]`, { + 'text-align': column.align + }); + } + // width + this.columnmanager.setColumnHeaderWidth(column.colIndex); + this.columnmanager.setColumnWidth(column.colIndex); + }); + this.setBodyStyle(); + } - $.style(this.bodyScrollable, { - width: width + 'px' - }); + refreshColumnWidth() { + this.datamanager.getColumns() + .map(column => { + this.columnmanager.setColumnHeaderWidth(column.colIndex); + this.columnmanager.setColumnWidth(column.colIndex); + }); + } - $.style(this.bodyScrollable, { - marginTop: $.style(this.header, 'height') + 'px' - }); + setBodyStyle() { + const width = $.style(this.header, 'width'); - $.style($('table', this.bodyScrollable), { - margin: 0 - }); - } + $.style(this.bodyScrollable, { + width: width + 'px' + }); - getColumnHeaderElement(colIndex) { - colIndex = +colIndex; - if (colIndex < 0) return null; - return $(`.data-table-col[data-col-index="${colIndex}"]`, this.header); - } + $.style(this.bodyScrollable, { + marginTop: $.style(this.header, 'height') + 'px' + }); + + $.style($('table', this.bodyScrollable), { + margin: 0 + }); + } + + getColumnHeaderElement(colIndex) { + colIndex = +colIndex; + if (colIndex < 0) return null; + return $(`.data-table-col[data-col-index="${colIndex}"]`, this.header); + } } diff --git a/src/utils.js b/src/utils.js index e2d2be5..e2969a1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,153 +2,153 @@ import _throttle from 'lodash/throttle'; import _debounce from 'lodash/debounce'; export function camelCaseToDash(str) { - return str.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); + return str.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); } export 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(); } export function getDefault(a, b) { - return a !== undefined ? a : b; + return a !== undefined ? a : b; } export function escapeRegExp(str) { - // https://stackoverflow.com/a/6969486 - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + // https://stackoverflow.com/a/6969486 + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); } export function getCSSString(styleMap) { - let style = ''; + let style = ''; - for (const prop in styleMap) { - if (styleMap.hasOwnProperty(prop)) { - style += `${prop}: ${styleMap[prop]}; `; + for (const prop in styleMap) { + if (styleMap.hasOwnProperty(prop)) { + style += `${prop}: ${styleMap[prop]}; `; + } } - } - return style.trim(); + return style.trim(); } export function getCSSRuleBlock(rule, styleMap) { - return `${rule} { ${getCSSString(styleMap)} }`; + return `${rule} { ${getCSSString(styleMap)} }`; } export function buildCSSRule(rule, styleMap, cssRulesString = '') { - // build css rules efficiently, - // append new rule if doesnt exist, - // update existing ones + // build css rules efficiently, + // append new rule if doesnt exist, + // update existing ones - const rulePatternStr = `${escapeRegExp(rule)} {([^}]*)}`; - const rulePattern = new RegExp(rulePatternStr, 'g'); + const rulePatternStr = `${escapeRegExp(rule)} {([^}]*)}`; + const rulePattern = new RegExp(rulePatternStr, 'g'); - if (cssRulesString && cssRulesString.match(rulePattern)) { - for (const property in styleMap) { - const value = styleMap[property]; - const propPattern = new RegExp(`${escapeRegExp(property)}:([^;]*);`); + if (cssRulesString && cssRulesString.match(rulePattern)) { + for (const property in styleMap) { + const value = styleMap[property]; + const propPattern = new RegExp(`${escapeRegExp(property)}:([^;]*);`); - cssRulesString = cssRulesString.replace(rulePattern, function (match, propertyStr) { - if (propertyStr.match(propPattern)) { - // property exists, replace value with new value - propertyStr = propertyStr.replace(propPattern, (match, valueStr) => { - return `${property}: ${value};`; - }); + cssRulesString = cssRulesString.replace(rulePattern, function (match, propertyStr) { + if (propertyStr.match(propPattern)) { + // property exists, replace value with new value + propertyStr = propertyStr.replace(propPattern, (match, valueStr) => { + return `${property}: ${value};`; + }); + } + propertyStr = propertyStr.trim(); + + const replacer = + `${rule} { ${propertyStr} }`; + + return replacer; + }); } - propertyStr = propertyStr.trim(); - const replacer = - `${rule} { ${propertyStr} }`; - - return replacer; - }); + return cssRulesString; } - - return cssRulesString; - } - // no match, append new rule block - return `${cssRulesString}${getCSSRuleBlock(rule, styleMap)}`; + // no match, append new rule block + return `${cssRulesString}${getCSSRuleBlock(rule, styleMap)}`; } export function removeCSSRule(rule, cssRulesString = '') { - const rulePatternStr = `${escapeRegExp(rule)} {([^}]*)}`; - const rulePattern = new RegExp(rulePatternStr, 'g'); - let output = cssRulesString; + const rulePatternStr = `${escapeRegExp(rule)} {([^}]*)}`; + const rulePattern = new RegExp(rulePatternStr, 'g'); + let output = cssRulesString; - if (cssRulesString && cssRulesString.match(rulePattern)) { - output = cssRulesString.replace(rulePattern, ''); - } + if (cssRulesString && cssRulesString.match(rulePattern)) { + output = cssRulesString.replace(rulePattern, ''); + } - return output.trim(); + return output.trim(); } export 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); } export function isNumeric(val) { - return !isNaN(val); + return !isNaN(val); } export let throttle = _throttle; @@ -156,30 +156,30 @@ export let throttle = _throttle; export let debounce = _debounce; export 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); + }); + }; }; export function chainPromises(promises) { - return promises.reduce( - (prev, cur) => prev.then(cur), Promise.resolve() - ); + return promises.reduce( + (prev, cur) => prev.then(cur), Promise.resolve() + ); }; export 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); };