diff --git a/dist/frappe-datatable.cjs.js b/dist/frappe-datatable.cjs.js index faa601a..79f2eef 100644 --- a/dist/frappe-datatable.cjs.js +++ b/dist/frappe-datatable.cjs.js @@ -242,10 +242,12 @@ 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. */ @@ -253,16 +255,34 @@ 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. */ @@ -291,11 +311,10 @@ function getRawTag(value) { try { value[symToStringTag] = undefined; - var unmasked = true; } catch (e) {} var result = nativeObjectToString.call(value); - if (unmasked) { + { if (isOwn) { value[symToStringTag] = tag; } else { @@ -330,8 +349,9 @@ function objectToString(value) { var _objectToString = objectToString; -var nullTag = '[object Null]'; -var undefinedTag = '[object Undefined]'; +/** `Object#toString` result references. */ +var nullTag = '[object Null]', + undefinedTag = '[object Undefined]'; /** Built-in value references. */ var symToStringTag$1 = _Symbol ? _Symbol.toStringTag : undefined; @@ -384,6 +404,7 @@ function isObjectLike(value) { var isObjectLike_1 = isObjectLike; +/** `Object#toString` result references. */ var symbolTag = '[object Symbol]'; /** @@ -410,6 +431,7 @@ function isSymbol(value) { var isSymbol_1 = isSymbol; +/** Used as references for various `Number` constants. */ var NAN = 0 / 0; /** Used to match leading and trailing whitespace. */ @@ -473,11 +495,12 @@ function toNumber(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; +var nativeMax = Math.max, + nativeMin = Math.min; /** * Creates a debounced function that delays invoking `func` until after `wait` @@ -659,6 +682,7 @@ function debounce(func, wait, options) { var debounce_1 = debounce; +/** Error message constants. */ var FUNC_ERROR_TEXT$1 = 'Expected a function'; /** @@ -805,7 +829,7 @@ function isNumeric(val) { let throttle$1 = throttle_1; -let debounce$2 = debounce_1; +let debounce$1 = debounce_1; function nextTick(fn, context = null) { return (...args) => { @@ -818,7 +842,6 @@ function nextTick(fn, context = null) { }); }; } - function linkProperties(target, source, properties) { const props = properties.reduce((acc, prop) => { acc[prop] = { @@ -830,7 +853,6 @@ function linkProperties(target, source, properties) { }, {}); Object.defineProperties(target, props); } - function isSet(val) { return val !== undefined || val !== null; } @@ -1226,7 +1248,7 @@ class DataManager { } } - const _row = this.prepareRow(row, rowIndex); + const _row = this.prepareRow(row, {rowIndex}); const index = this.rows.findIndex(row => row[0].rowIndex === rowIndex); this.rows[index] = _row; @@ -1606,6 +1628,22 @@ class CellManager { this.instance.showToastMessage(message, 2); } }); + + if (this.options.pasteFromClipboard) { + this.keyboard.on('ctrl+v', (e) => { + // hack + // https://stackoverflow.com/a/2177059/5353542 + this.instance.pasteTarget.focus(); + + setTimeout(() => { + const data = this.instance.pasteTarget.value; + this.instance.pasteTarget.value = ''; + this.pasteContentInCell(data); + }, 10); + + return false; + }); + } } bindMouseEvents() { @@ -1994,6 +2032,30 @@ class CellManager { return rows.reduce((total, row) => total + row.length, 0); } + pasteContentInCell(data) { + if (!this.$focusedCell) return; + + const matrix = data + .split('\n') + .map(row => row.split('\t')) + .filter(row => row.length && row.every(it => it)); + + let { colIndex, rowIndex } = $.data(this.$focusedCell); + + let focusedCell = { + colIndex: +colIndex, + rowIndex: +rowIndex + }; + + matrix.forEach((row, i) => { + let rowIndex = i + focusedCell.rowIndex; + row.forEach((cell, j) => { + let colIndex = j + focusedCell.colIndex; + this.updateCell(colIndex, rowIndex, cell); + }); + }); + } + activateFilter(colIndex) { this.columnmanager.toggleFilter(); this.columnmanager.focusFilter(colIndex); @@ -2513,7 +2575,7 @@ class ColumnManager { this.rowmanager.showRows(rowsToShow); }); }; - $.on(this.header, 'keydown', '.dt-filter', debounce$2(handler, 300)); + $.on(this.header, 'keydown', '.dt-filter', debounce$1(handler, 300)); } sortRows(colIndex, sortOrder) { @@ -3299,7 +3361,8 @@ const KEYCODES = { 9: 'tab', 27: 'esc', 67: 'c', - 70: 'f' + 70: 'f', + 86: 'v' }; class Keyboard { @@ -3394,7 +3457,8 @@ var DEFAULT_OPTIONS = { inlineFilters: false, treeView: false, checkedRowStatus: true, - dynamicRowHeight: false + dynamicRowHeight: false, + pasteFromClipboard: false }; class DataTable { @@ -3434,8 +3498,11 @@ class DataTable { this.options || {}, options ); - this.options.headerDropdown - .push(...(options.headerDropdown || [])); + options.headerDropdown = options.headerDropdown || []; + this.options.headerDropdown = [ + ...DEFAULT_OPTIONS.headerDropdown, + ...options.headerDropdown + ]; // custom user events this.events = Object.assign( @@ -3464,6 +3531,7 @@ class DataTable {
+ `; @@ -3472,6 +3540,7 @@ class DataTable { this.bodyScrollable = $('.dt-scrollable', this.wrapper); this.freezeContainer = $('.dt-freeze', this.wrapper); this.toastMessage = $('.dt-toast', this.wrapper); + this.pasteTarget = $('.dt-paste-target', this.wrapper); } refresh(data, columns) { @@ -3589,7 +3658,7 @@ class DataTable { DataTable.instances = 0; var name = "frappe-datatable"; -var version = "0.0.4"; +var version = "0.0.5"; var description = "A modern datatable library for the web"; var main = "dist/frappe-datatable.cjs.js"; var scripts = {"start":"yarn run dev","build":"rollup -c","production":"rollup -c --production","build:docs":"rollup -c --docs","dev":"rollup -c -w","test":"mocha --compilers js:babel-core/register --colors ./test/*.spec.js"}; diff --git a/dist/frappe-datatable.css b/dist/frappe-datatable.css index c4253e8..edffe35 100644 --- a/dist/frappe-datatable.css +++ b/dist/frappe-datatable.css @@ -247,6 +247,11 @@ transform: translateY(-50%); } +.dt-paste-target { + position: fixed; + left: -999em; +} + body.dt-resize { cursor: col-resize; } diff --git a/dist/frappe-datatable.js b/dist/frappe-datatable.js index 60b8f48..5f1ec6f 100644 --- a/dist/frappe-datatable.js +++ b/dist/frappe-datatable.js @@ -1,2195 +1,2257 @@ var DataTable = (function (Sortable,Clusterize) { -'use strict'; + 'use strict'; -Sortable = Sortable && Sortable.hasOwnProperty('default') ? Sortable['default'] : Sortable; -Clusterize = Clusterize && Clusterize.hasOwnProperty('default') ? Clusterize['default'] : Clusterize; + 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; -} + 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; -}; + $.each = (expr, con) => { + return typeof expr === 'string' ? + Array.from((con || document).querySelectorAll(expr)) : + expr || null; + }; -$.create = (tag, o) => { - let element = document.createElement(tag); + $.create = (tag, o) => { + 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 === '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); } - } 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; - }); -}; - -$.scrollbarWidth = function scrollbarWidth() { - // Create the measurement node - const scrollDiv = document.createElement('div'); - $.style(scrollDiv, { - width: '100px', - height: '100px', - overflow: 'scroll', - position: 'absolute', - top: '-9999px' - }); - document.body.appendChild(scrollDiv); - - // Get the scrollbar width - const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; - - // Delete the DIV - document.body.removeChild(scrollDiv); - - return scrollbarWidth; -}; - -/** - * Checks if `value` is the - * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) - * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an object, else `false`. - * @example - * - * _.isObject({}); - * // => true - * - * _.isObject([1, 2, 3]); - * // => true - * - * _.isObject(_.noop); - * // => true - * - * _.isObject(null); - * // => false - */ -function isObject(value) { - var type = typeof value; - return value != null && (type == 'object' || type == 'function'); -} - -var isObject_1 = isObject; - -var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; - -var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; - -var _freeGlobal = freeGlobal; - -var freeSelf = typeof self == 'object' && self && self.Object === Object && self; - -/** Used as a reference to the global object. */ -var root = _freeGlobal || freeSelf || Function('return this')(); - -var _root = root; - -var now = function() { - return _root.Date.now(); -}; - -var now_1 = now; - -var Symbol = _root.Symbol; - -var _Symbol = Symbol; - -var objectProto = Object.prototype; - -/** Used to check objects for own properties. */ -var hasOwnProperty = objectProto.hasOwnProperty; - -/** - * Used to resolve the - * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) - * of values. - */ -var nativeObjectToString = objectProto.toString; - -/** Built-in value references. */ -var symToStringTag = _Symbol ? _Symbol.toStringTag : undefined; - -/** - * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. - * - * @private - * @param {*} value The value to query. - * @returns {string} Returns the raw `toStringTag`. - */ -function getRawTag(value) { - var isOwn = hasOwnProperty.call(value, symToStringTag), - tag = value[symToStringTag]; - - try { - value[symToStringTag] = undefined; - var unmasked = true; - } catch (e) {} - - var result = nativeObjectToString.call(value); - if (unmasked) { - if (isOwn) { - value[symToStringTag] = tag; - } else { - delete value[symToStringTag]; - } - } - return result; -} - -var _getRawTag = getRawTag; - -/** Used for built-in method references. */ -var objectProto$1 = Object.prototype; - -/** - * Used to resolve the - * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) - * of values. - */ -var nativeObjectToString$1 = objectProto$1.toString; - -/** - * Converts `value` to a string using `Object.prototype.toString`. - * - * @private - * @param {*} value The value to convert. - * @returns {string} Returns the converted string. - */ -function objectToString(value) { - return nativeObjectToString$1.call(value); -} - -var _objectToString = objectToString; - -var nullTag = '[object Null]'; -var undefinedTag = '[object Undefined]'; - -/** Built-in value references. */ -var symToStringTag$1 = _Symbol ? _Symbol.toStringTag : undefined; - -/** - * The base implementation of `getTag` without fallbacks for buggy environments. - * - * @private - * @param {*} value The value to query. - * @returns {string} Returns the `toStringTag`. - */ -function baseGetTag(value) { - if (value == null) { - return value === undefined ? undefinedTag : nullTag; - } - return (symToStringTag$1 && symToStringTag$1 in Object(value)) - ? _getRawTag(value) - : _objectToString(value); -} - -var _baseGetTag = baseGetTag; - -/** - * Checks if `value` is object-like. A value is object-like if it's not `null` - * and has a `typeof` result of "object". - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is object-like, else `false`. - * @example - * - * _.isObjectLike({}); - * // => true - * - * _.isObjectLike([1, 2, 3]); - * // => true - * - * _.isObjectLike(_.noop); - * // => false - * - * _.isObjectLike(null); - * // => false - */ -function isObjectLike(value) { - return value != null && typeof value == 'object'; -} - -var isObjectLike_1 = isObjectLike; - -var symbolTag = '[object Symbol]'; - -/** - * Checks if `value` is classified as a `Symbol` primitive or object. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. - * @example - * - * _.isSymbol(Symbol.iterator); - * // => true - * - * _.isSymbol('abc'); - * // => false - */ -function isSymbol(value) { - return typeof value == 'symbol' || - (isObjectLike_1(value) && _baseGetTag(value) == symbolTag); -} - -var isSymbol_1 = isSymbol; - -var NAN = 0 / 0; - -/** Used to match leading and trailing whitespace. */ -var reTrim = /^\s+|\s+$/g; - -/** Used to detect bad signed hexadecimal string values. */ -var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; - -/** Used to detect binary string values. */ -var reIsBinary = /^0b[01]+$/i; - -/** Used to detect octal string values. */ -var reIsOctal = /^0o[0-7]+$/i; - -/** Built-in method references without a dependency on `root`. */ -var freeParseInt = parseInt; - -/** - * Converts `value` to a number. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to process. - * @returns {number} Returns the number. - * @example - * - * _.toNumber(3.2); - * // => 3.2 - * - * _.toNumber(Number.MIN_VALUE); - * // => 5e-324 - * - * _.toNumber(Infinity); - * // => Infinity - * - * _.toNumber('3.2'); - * // => 3.2 - */ -function toNumber(value) { - if (typeof value == 'number') { - return value; - } - if (isSymbol_1(value)) { - return NAN; - } - if (isObject_1(value)) { - var other = typeof value.valueOf == 'function' ? value.valueOf() : value; - value = isObject_1(other) ? (other + '') : other; - } - if (typeof value != 'string') { - return value === 0 ? value : +value; - } - value = value.replace(reTrim, ''); - var isBinary = reIsBinary.test(value); - return (isBinary || reIsOctal.test(value)) - ? freeParseInt(value.slice(2), isBinary ? 2 : 8) - : (reIsBadHex.test(value) ? NAN : +value); -} - -var toNumber_1 = toNumber; - -var FUNC_ERROR_TEXT = 'Expected a function'; - -/* Built-in method references for those with the same name as other `lodash` methods. */ -var nativeMax = Math.max; -var nativeMin = Math.min; - -/** - * Creates a debounced function that delays invoking `func` until after `wait` - * milliseconds have elapsed since the last time the debounced function was - * invoked. The debounced function comes with a `cancel` method to cancel - * delayed `func` invocations and a `flush` method to immediately invoke them. - * Provide `options` to indicate whether `func` should be invoked on the - * leading and/or trailing edge of the `wait` timeout. The `func` is invoked - * with the last arguments provided to the debounced function. Subsequent - * calls to the debounced function return the result of the last `func` - * invocation. - * - * **Note:** If `leading` and `trailing` options are `true`, `func` is - * invoked on the trailing edge of the timeout only if the debounced function - * is invoked more than once during the `wait` timeout. - * - * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred - * until to the next tick, similar to `setTimeout` with a timeout of `0`. - * - * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) - * for details over the differences between `_.debounce` and `_.throttle`. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Function - * @param {Function} func The function to debounce. - * @param {number} [wait=0] The number of milliseconds to delay. - * @param {Object} [options={}] The options object. - * @param {boolean} [options.leading=false] - * Specify invoking on the leading edge of the timeout. - * @param {number} [options.maxWait] - * The maximum time `func` is allowed to be delayed before it's invoked. - * @param {boolean} [options.trailing=true] - * Specify invoking on the trailing edge of the timeout. - * @returns {Function} Returns the new debounced function. - * @example - * - * // Avoid costly calculations while the window size is in flux. - * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); - * - * // Invoke `sendMail` when clicked, debouncing subsequent calls. - * jQuery(element).on('click', _.debounce(sendMail, 300, { - * 'leading': true, - * 'trailing': false - * })); - * - * // Ensure `batchLog` is invoked once after 1 second of debounced calls. - * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); - * var source = new EventSource('/stream'); - * jQuery(source).on('message', debounced); - * - * // Cancel the trailing debounced invocation. - * jQuery(window).on('popstate', debounced.cancel); - */ -function debounce(func, wait, options) { - var lastArgs, - lastThis, - maxWait, - result, - timerId, - lastCallTime, - lastInvokeTime = 0, - leading = false, - maxing = false, - trailing = true; - - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - wait = toNumber_1(wait) || 0; - if (isObject_1(options)) { - leading = !!options.leading; - maxing = 'maxWait' in options; - maxWait = maxing ? nativeMax(toNumber_1(options.maxWait) || 0, wait) : maxWait; - trailing = 'trailing' in options ? !!options.trailing : trailing; - } - - function invokeFunc(time) { - var args = lastArgs, - thisArg = lastThis; - - lastArgs = lastThis = undefined; - lastInvokeTime = time; - result = func.apply(thisArg, args); - return result; - } - - function leadingEdge(time) { - // Reset any `maxWait` timer. - lastInvokeTime = time; - // Start the timer for the trailing edge. - timerId = setTimeout(timerExpired, wait); - // Invoke the leading edge. - return leading ? invokeFunc(time) : result; - } - - function remainingWait(time) { - var timeSinceLastCall = time - lastCallTime, - timeSinceLastInvoke = time - lastInvokeTime, - timeWaiting = wait - timeSinceLastCall; - - return maxing - ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) - : timeWaiting; - } - - function shouldInvoke(time) { - var timeSinceLastCall = time - lastCallTime, - timeSinceLastInvoke = time - lastInvokeTime; - - // Either this is the first call, activity has stopped and we're at the - // trailing edge, the system time has gone backwards and we're treating - // it as the trailing edge, or we've hit the `maxWait` limit. - return (lastCallTime === undefined || (timeSinceLastCall >= wait) || - (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); - } - - function timerExpired() { - var time = now_1(); - if (shouldInvoke(time)) { - return trailingEdge(time); - } - // Restart the timer. - timerId = setTimeout(timerExpired, remainingWait(time)); - } - - function trailingEdge(time) { - timerId = undefined; - - // Only invoke if we have `lastArgs` which means `func` has been - // debounced at least once. - if (trailing && lastArgs) { - return invokeFunc(time); - } - lastArgs = lastThis = undefined; - return result; - } - - function cancel() { - if (timerId !== undefined) { - clearTimeout(timerId); - } - lastInvokeTime = 0; - lastArgs = lastCallTime = lastThis = timerId = undefined; - } - - function flush() { - return timerId === undefined ? result : trailingEdge(now_1()); - } - - function debounced() { - var time = now_1(), - isInvoking = shouldInvoke(time); - - lastArgs = arguments; - lastThis = this; - lastCallTime = time; - - if (isInvoking) { - if (timerId === undefined) { - return leadingEdge(lastCallTime); - } - if (maxing) { - // Handle invocations in a tight loop. - timerId = setTimeout(timerExpired, wait); - return invokeFunc(lastCallTime); - } - } - if (timerId === undefined) { - timerId = setTimeout(timerExpired, wait); - } - return result; - } - debounced.cancel = cancel; - debounced.flush = flush; - return debounced; -} - -var debounce_1 = debounce; - -var FUNC_ERROR_TEXT$1 = 'Expected a function'; - -/** - * Creates a throttled function that only invokes `func` at most once per - * every `wait` milliseconds. The throttled function comes with a `cancel` - * method to cancel delayed `func` invocations and a `flush` method to - * immediately invoke them. Provide `options` to indicate whether `func` - * should be invoked on the leading and/or trailing edge of the `wait` - * timeout. The `func` is invoked with the last arguments provided to the - * throttled function. Subsequent calls to the throttled function return the - * result of the last `func` invocation. - * - * **Note:** If `leading` and `trailing` options are `true`, `func` is - * invoked on the trailing edge of the timeout only if the throttled function - * is invoked more than once during the `wait` timeout. - * - * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred - * until to the next tick, similar to `setTimeout` with a timeout of `0`. - * - * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) - * for details over the differences between `_.throttle` and `_.debounce`. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Function - * @param {Function} func The function to throttle. - * @param {number} [wait=0] The number of milliseconds to throttle invocations to. - * @param {Object} [options={}] The options object. - * @param {boolean} [options.leading=true] - * Specify invoking on the leading edge of the timeout. - * @param {boolean} [options.trailing=true] - * Specify invoking on the trailing edge of the timeout. - * @returns {Function} Returns the new throttled function. - * @example - * - * // Avoid excessively updating the position while scrolling. - * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); - * - * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. - * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); - * jQuery(element).on('click', throttled); - * - * // Cancel the trailing throttled invocation. - * jQuery(window).on('popstate', throttled.cancel); - */ -function throttle(func, wait, options) { - var leading = true, - trailing = true; - - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT$1); - } - if (isObject_1(options)) { - leading = 'leading' in options ? !!options.leading : leading; - trailing = 'trailing' in options ? !!options.trailing : trailing; - } - return debounce_1(func, wait, { - 'leading': leading, - 'maxWait': wait, - 'trailing': trailing - }); -} - -var throttle_1 = throttle; - -function camelCaseToDash(str) { - return str.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); -} - -function makeDataAttributeString(props) { - const keys = Object.keys(props); - - return keys - .map((key) => { - const _key = camelCaseToDash(key); - const val = props[key]; - - if (val === undefined) return ''; - return `data-${_key}="${val}" `; - }) - .join('') - .trim(); -} - -function 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 nextTick(fn, context = null) { - return (...args) => { - return new Promise(resolve => { - const execute = () => { - const out = fn.apply(context, args); - resolve(out); - }; - setTimeout(execute); + }; + + $.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); }); }; -} -function linkProperties(target, source, properties) { - const props = properties.reduce((acc, prop) => { - acc[prop] = { - get() { - return source[prop]; - } - }; - return acc; - }, {}); - Object.defineProperties(target, props); -} - -function isSet(val) { - return val !== undefined || val !== null; -} - -function notSet(val) { - return !isSet(val); -} - -function isNumber(val) { - return !isNaN(val); -} - -function ensureArray(val) { - if (!Array.isArray(val)) { - return [val]; - } - return val; -} - -class DataManager { - constructor(options) { - this.options = options; - this.sortRows = nextTick(this.sortRows, this); - this.switchColumn = nextTick(this.switchColumn, this); - this.removeColumn = nextTick(this.removeColumn, this); - this.filterRows = nextTick(this.filterRows, this); - } - - init(data, columns) { - if (!data) { - data = this.options.data; - } - if (columns) { - this.options.columns = columns; - } - - this.data = data; - - this.rowCount = 0; - this.columns = []; - this.rows = []; - - this.prepareColumns(); - this.prepareRows(); - this.prepareTreeRows(); - this.prepareRowView(); - - 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.checkboxColumn && !this.hasColumnById('_checkbox')) { - const cell = { - id: '_checkbox', - content: this.getCheckboxHTML(), - editable: false, - resizable: false, - sortable: false, - focusable: false, - dropdown: false, - width: 32 - }; - this.columns.push(cell); - } - - if (this.options.serialNoColumn && !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.content = col.content || col.name || ''; - col.id = col.id || col.content; - return col; - }); - } - - prepareCell(content, i) { - const cell = { - content: '', - 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 = []; - let meta = { - rowIndex: index - }; - - if (Array.isArray(d)) { - // row is an array - if (this.options.checkboxColumn) { - row.push(this.getCheckboxHTML()); - } - if (this.options.serialNoColumn) { - 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]); - } - } - - meta.indent = d.indent || 0; - } - - return this.prepareRow(row, meta); - }); - } - - prepareTreeRows() { - this.rows.forEach((row, i) => { - if (isNumber(row.meta.indent)) { - // if (i === 36) debugger; - const nextRow = this.getRow(i + 1); - row.meta.isLeaf = !nextRow || - notSet(nextRow.meta.indent) || - nextRow.meta.indent <= row.meta.indent; + $.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); } }); - } + }; - prepareRowView() { - // This is order in which rows will be rendered in the table. - // When sorting happens, only this.rowViewOrder will change - // and not the original this.rows - this.rowViewOrder = this.rows.map(row => row.meta.rowIndex); - } + $.unbind = (element, o) => { + if (element) { + for (let event in o) { + let callback = o[event]; - prepareRow(row, meta) { - const baseRowCell = { - rowIndex: meta.rowIndex, - indent: meta.indent - }; + event.split(/\s+/).forEach(function (event) { + element.removeEventListener(event, callback); + }); + } + } + }; - row = row - .map((cell, i) => this.prepareCell(cell, i)) - .map(cell => Object.assign({}, baseRowCell, cell)); + $.fire = (target, type, properties) => { + let evt = document.createEvent('HTMLEvents'); - // monkey patched in array object - row.meta = meta; - return row; - } + evt.initEvent(type, true, true); - validateColumns() { - const columns = this.options.columns; - if (!Array.isArray(columns)) { - throw new DataError('`columns` must be an array'); + for (let j in properties) { + evt[j] = properties[j]; } - columns.forEach((column, i) => { - if (typeof column !== 'string' && typeof column !== 'object') { - throw new DataError(`column "${i}" must be a string or an object`); + 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]; } }); - } + }; - 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.push(...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.rowViewOrder); - this.currentSort.sortOrder = sortOrder; - return; - } + $.removeStyle = (elements, styleProps) => { + if (!Array.isArray(elements)) { + elements = [elements]; } - this.rowViewOrder.sort((a, b) => { - const aIndex = a; - const bIndex = b; - const aContent = this.getCell(colIndex, a).content; - const bContent = this.getCell(colIndex, b).content; - - if (sortOrder === 'none') { - return aIndex - bIndex; - } else if (sortOrder === 'asc') { - if (aContent < bContent) return -1; - if (aContent > bContent) return 1; - if (aContent === bContent) return 0; - } else if (sortOrder === 'desc') { - if (aContent < bContent) return 1; - if (aContent > bContent) return -1; - if (aContent === bContent) return 0; - } - return 0; - }); - - if (this.hasColumnById('_rowIndex')) { - // update row index - const srNoColIndex = this.getColumnIndexById('_rowIndex'); - this.rows.forEach((row, index) => { - const viewIndex = this.rowViewOrder.indexOf(index); - const cell = row[srNoColIndex]; - cell.content = (viewIndex + 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.forEach(row => { - const newCell1 = Object.assign({}, row[index1], { - colIndex: index2 - }); - const newCell2 = Object.assign({}, row[index2], { - colIndex: index1 - }); - - row[index2] = newCell1; - row[index1] = newCell2; - }); - } - - 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.forEach(row => { - // remove cell - row.splice(index, 1); - // update colIndex - row.forEach((cell, i) => { - cell.colIndex = i; - }); - }); - } - - 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); - } + if (!Array.isArray(styleProps)) { + styleProps = [styleProps]; } - 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 = String(cell.content || '').toLowerCase(); - const needle = (keyword || '').toLowerCase(); - - if (!needle || hay.includes(needle)) { - rowsToShow.push(cell.rowIndex); - } else { - rowsToHide.push(cell.rowIndex); + elements.map(element => { + for (const prop of styleProps) { + element.style[prop] = ''; } }); + }; - this._filteredRows = rowsToShow; + $.getStyle = (element, prop) => { + let val = getComputedStyle(element)[prop]; - return { - rowsToHide, - rowsToShow - }; - } - - getFilteredRowIndices() { - return this._filteredRows || this.rows.map(row => row.meta.rowIndex); - } - - getRowCount() { - return this.rowCount; - } - - _getNextRowCount() { - const val = this.rowCount; - - this.rowCount++; - return val; - } - - getRows(start, end) { - return this.rows.slice(start, end); - } - - getRowsForView(start, end) { - const rows = this.rowViewOrder.map(i => this.rows[i]); - return rows.slice(start, end); - } - - getColumns(skipStandardColumns) { - let columns = this.columns; - - if (skipStandardColumns) { - columns = columns.slice(this.getStandardColumnCount()); - } - - return columns; - } - - getStandardColumnCount() { - if (this.options.checkboxColumn && this.options.serialNoColumn) { - return 2; - } - - if (this.options.checkboxColumn || this.options.serialNoColumn) { - return 1; - } - - return 0; - } - - getColumnCount(skipStandardColumns) { - let val = this.columns.length; - - if (skipStandardColumns) { - val = val - this.getStandardColumnCount(); + if (['width', 'height'].includes(prop)) { + val = parseFloat(val); } return val; - } + }; - getColumn(colIndex) { - colIndex = +colIndex; + $.closest = (selector, element) => { + if (!element) return null; - if (colIndex < 0) { - // negative indexes - colIndex = this.columns.length + colIndex; + if (element.matches(selector)) { + return element; } - return this.columns.find(col => col.colIndex === colIndex); - } + return $.closest(selector, element.parentNode); + }; - getColumnById(id) { - return this.columns.find(col => col.id === id); - } + $.inViewport = (el, parentEl) => { + const { + top, + left, + bottom, + right + } = el.getBoundingClientRect(); + const { + top: pTop, + left: pLeft, + bottom: pBottom, + right: pRight + } = parentEl.getBoundingClientRect(); - getRow(rowIndex) { - rowIndex = +rowIndex; - return this.rows[rowIndex]; - } + return top >= pTop && left >= pLeft && bottom <= pBottom && right <= pRight; + }; - getCell(colIndex, rowIndex) { - rowIndex = +rowIndex; - colIndex = +colIndex; - return this.getRow(rowIndex)[colIndex]; - } + $.scrollTop = function scrollTop(element, pixels) { + requestAnimationFrame(() => { + element.scrollTop = pixels; + }); + }; - getChildren(parentRowIndex) { - parentRowIndex = +parentRowIndex; - const parentIndent = this.getRow(parentRowIndex).meta.indent; - const out = []; + $.scrollbarWidth = function scrollbarWidth() { + // Create the measurement node + const scrollDiv = document.createElement('div'); + $.style(scrollDiv, { + width: '100px', + height: '100px', + overflow: 'scroll', + position: 'absolute', + top: '-9999px' + }); + document.body.appendChild(scrollDiv); - for (let i = parentRowIndex + 1; i < this.rowCount; i++) { - const row = this.getRow(i); - if (isNaN(row.meta.indent)) continue; + // Get the scrollbar width + const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; - if (row.meta.indent > parentIndent) { - out.push(i); - } + // Delete the DIV + document.body.removeChild(scrollDiv); - if (row.meta.indent === parentIndent) { - break; - } - } - - return out; - } - - getImmediateChildren(parentRowIndex) { - parentRowIndex = +parentRowIndex; - const parentIndent = this.getRow(parentRowIndex).meta.indent; - const out = []; - const childIndent = parentIndent + 1; - - for (let i = parentRowIndex + 1; i < this.rowCount; i++) { - const row = this.getRow(i); - if (isNaN(row.meta.indent) || row.meta.indent > childIndent) continue; - - if (row.meta.indent === childIndent) { - out.push(i); - } - - if (row.meta.indent === parentIndent) { - break; - } - } - - return out; - } - - get() { - return { - columns: this.columns, - rows: this.rows - }; - } + return scrollbarWidth; + }; /** - * Returns the original data which was passed - * based on rowIndex - * @param {Number} rowIndex - * @returns Array|Object - * @memberof DataManager + * 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 */ - getData(rowIndex) { - return this.data[rowIndex]; + function isObject(value) { + var type = typeof value; + return value != null && (type == 'object' || type == 'function'); } - hasColumn(name) { - return Boolean(this.columns.find(col => col.content === name)); + 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; + } catch (e) {} + + var result = nativeObjectToString.call(value); + { + if (isOwn) { + value[symToStringTag] = tag; + } else { + delete value[symToStringTag]; + } + } + return result; } - hasColumnById(id) { - return Boolean(this.columns.find(col => col.id === id)); + 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); } - getColumnIndex(name) { - return this.columns.findIndex(col => col.content === name); + var _objectToString = objectToString; + + /** `Object#toString` result references. */ + var nullTag = '[object Null]', + 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); } - getColumnIndexById(id) { - return this.columns.findIndex(col => col.id === id); + 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'; } - getCheckboxHTML() { - return ''; - } -} + var isObjectLike_1 = isObjectLike; -// Custom Errors -class DataError extends TypeError {} + /** `Object#toString` result references. */ + var symbolTag = '[object Symbol]'; -class CellManager { - constructor(instance) { - this.instance = instance; - linkProperties(this, this.instance, [ - 'wrapper', - 'options', - 'style', - 'bodyScrollable', - 'columnmanager', - 'rowmanager', - 'datamanager', - 'keyboard' - ]); - - this.bindEvents(); + /** + * 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); } - bindEvents() { - this.bindFocusCell(); - this.bindEditCell(); - this.bindKeyboardSelection(); - this.bindCopyCellContents(); - this.bindMouseEvents(); - this.bindTreeEvents(); + 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); } - bindFocusCell() { - this.bindKeyboardNav(); + 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, + 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; } - bindEditCell() { - this.$editingCell = null; + var debounce_1 = debounce; - $.on(this.bodyScrollable, 'dblclick', '.dt-cell', (e, cell) => { - this.activateEditing(cell); - }); + /** Error message constants. */ + var FUNC_ERROR_TEXT$1 = 'Expected a function'; - this.keyboard.on('enter', () => { - 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(); - } - }); + /** + * 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 + }); } - bindKeyboardNav() { - const focusCell = (direction) => { - if (!this.$focusedCell || this.$editingCell) { - return false; - } + var throttle_1 = throttle; - let $cell = this.$focusedCell; + function camelCaseToDash(str) { + return str.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); + } - 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); - } + function makeDataAttributeString(props) { + const keys = Object.keys(props); - this.focusCell($cell); - return true; + return keys + .map((key) => { + const _key = camelCaseToDash(key); + const val = props[key]; + + if (val === undefined) return ''; + return `data-${_key}="${val}" `; + }) + .join('') + .trim(); + } + + 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$1 = debounce_1; + + function nextTick(fn, context = null) { + return (...args) => { + return new Promise(resolve => { + const execute = () => { + const out = fn.apply(context, args); + resolve(out); + }; + setTimeout(execute); + }); }; + } + function linkProperties(target, source, properties) { + const props = properties.reduce((acc, prop) => { + acc[prop] = { + get() { + return source[prop]; + } + }; + return acc; + }, {}); + Object.defineProperties(target, props); + } + function isSet(val) { + return val !== undefined || val !== null; + } - const focusLastCell = (direction) => { - if (!this.$focusedCell || this.$editingCell) { + function notSet(val) { + return !isSet(val); + } + + function isNumber(val) { + return !isNaN(val); + } + + function ensureArray(val) { + if (!Array.isArray(val)) { + return [val]; + } + return val; + } + + class DataManager { + constructor(options) { + this.options = options; + this.sortRows = nextTick(this.sortRows, this); + this.switchColumn = nextTick(this.switchColumn, this); + this.removeColumn = nextTick(this.removeColumn, this); + this.filterRows = nextTick(this.filterRows, this); + } + + init(data, columns) { + if (!data) { + data = this.options.data; + } + if (columns) { + this.options.columns = columns; + } + + this.data = data; + + this.rowCount = 0; + this.columns = []; + this.rows = []; + + this.prepareColumns(); + this.prepareRows(); + this.prepareTreeRows(); + this.prepareRowView(); + + 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.checkboxColumn && !this.hasColumnById('_checkbox')) { + const cell = { + id: '_checkbox', + content: this.getCheckboxHTML(), + editable: false, + resizable: false, + sortable: false, + focusable: false, + dropdown: false, + width: 32 + }; + this.columns.push(cell); + } + + if (this.options.serialNoColumn && !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.content = col.content || col.name || ''; + col.id = col.id || col.content; + return col; + }); + } + + prepareCell(content, i) { + const cell = { + content: '', + 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 = []; + let meta = { + rowIndex: index + }; + + if (Array.isArray(d)) { + // row is an array + if (this.options.checkboxColumn) { + row.push(this.getCheckboxHTML()); + } + if (this.options.serialNoColumn) { + 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]); + } + } + + meta.indent = d.indent || 0; + } + + return this.prepareRow(row, meta); + }); + } + + prepareTreeRows() { + this.rows.forEach((row, i) => { + if (isNumber(row.meta.indent)) { + // if (i === 36) debugger; + const nextRow = this.getRow(i + 1); + row.meta.isLeaf = !nextRow || + notSet(nextRow.meta.indent) || + nextRow.meta.indent <= row.meta.indent; + } + }); + } + + prepareRowView() { + // This is order in which rows will be rendered in the table. + // When sorting happens, only this.rowViewOrder will change + // and not the original this.rows + this.rowViewOrder = this.rows.map(row => row.meta.rowIndex); + } + + prepareRow(row, meta) { + const baseRowCell = { + rowIndex: meta.rowIndex, + indent: meta.indent + }; + + row = row + .map((cell, i) => this.prepareCell(cell, i)) + .map(cell => Object.assign({}, baseRowCell, cell)); + + // monkey patched in array object + row.meta = meta; + 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.push(...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.rowViewOrder); + this.currentSort.sortOrder = sortOrder; + return; + } + } + + this.rowViewOrder.sort((a, b) => { + const aIndex = a; + const bIndex = b; + const aContent = this.getCell(colIndex, a).content; + const bContent = this.getCell(colIndex, b).content; + + if (sortOrder === 'none') { + return aIndex - bIndex; + } else if (sortOrder === 'asc') { + if (aContent < bContent) return -1; + if (aContent > bContent) return 1; + if (aContent === bContent) return 0; + } else if (sortOrder === 'desc') { + if (aContent < bContent) return 1; + if (aContent > bContent) return -1; + if (aContent === bContent) return 0; + } + return 0; + }); + + if (this.hasColumnById('_rowIndex')) { + // update row index + const srNoColIndex = this.getColumnIndexById('_rowIndex'); + this.rows.forEach((row, index) => { + const viewIndex = this.rowViewOrder.indexOf(index); + const cell = row[srNoColIndex]; + cell.content = (viewIndex + 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.forEach(row => { + const newCell1 = Object.assign({}, row[index1], { + colIndex: index2 + }); + const newCell2 = Object.assign({}, row[index2], { + colIndex: index1 + }); + + row[index2] = newCell1; + row[index1] = newCell2; + }); + } + + 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.forEach(row => { + // remove cell + row.splice(index, 1); + // update colIndex + row.forEach((cell, i) => { + cell.colIndex = i; + }); + }); + } + + 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 = String(cell.content || '').toLowerCase(); + const needle = (keyword || '').toLowerCase(); + + if (!needle || hay.includes(needle)) { + rowsToShow.push(cell.rowIndex); + } else { + rowsToHide.push(cell.rowIndex); + } + }); + + this._filteredRows = rowsToShow; + + return { + rowsToHide, + rowsToShow + }; + } + + getFilteredRowIndices() { + return this._filteredRows || this.rows.map(row => row.meta.rowIndex); + } + + getRowCount() { + return this.rowCount; + } + + _getNextRowCount() { + const val = this.rowCount; + + this.rowCount++; + return val; + } + + getRows(start, end) { + return this.rows.slice(start, end); + } + + getRowsForView(start, end) { + const rows = this.rowViewOrder.map(i => this.rows[i]); + return rows.slice(start, end); + } + + getColumns(skipStandardColumns) { + let columns = this.columns; + + if (skipStandardColumns) { + columns = columns.slice(this.getStandardColumnCount()); + } + + return columns; + } + + getStandardColumnCount() { + if (this.options.checkboxColumn && this.options.serialNoColumn) { + return 2; + } + + if (this.options.checkboxColumn || this.options.serialNoColumn) { + return 1; + } + + return 0; + } + + getColumnCount(skipStandardColumns) { + let val = this.columns.length; + + if (skipStandardColumns) { + val = val - this.getStandardColumnCount(); + } + + return val; + } + + getColumn(colIndex) { + colIndex = +colIndex; + + if (colIndex < 0) { + // negative indexes + colIndex = this.columns.length + colIndex; + } + + return this.columns.find(col => col.colIndex === colIndex); + } + + getColumnById(id) { + return this.columns.find(col => col.id === id); + } + + getRow(rowIndex) { + rowIndex = +rowIndex; + return this.rows[rowIndex]; + } + + getCell(colIndex, rowIndex) { + rowIndex = +rowIndex; + colIndex = +colIndex; + return this.getRow(rowIndex)[colIndex]; + } + + getChildren(parentRowIndex) { + parentRowIndex = +parentRowIndex; + const parentIndent = this.getRow(parentRowIndex).meta.indent; + const out = []; + + for (let i = parentRowIndex + 1; i < this.rowCount; i++) { + const row = this.getRow(i); + if (isNaN(row.meta.indent)) continue; + + if (row.meta.indent > parentIndent) { + out.push(i); + } + + if (row.meta.indent === parentIndent) { + break; + } + } + + return out; + } + + getImmediateChildren(parentRowIndex) { + parentRowIndex = +parentRowIndex; + const parentIndent = this.getRow(parentRowIndex).meta.indent; + const out = []; + const childIndent = parentIndent + 1; + + for (let i = parentRowIndex + 1; i < this.rowCount; i++) { + const row = this.getRow(i); + if (isNaN(row.meta.indent) || row.meta.indent > childIndent) continue; + + if (row.meta.indent === childIndent) { + out.push(i); + } + + if (row.meta.indent === parentIndent) { + break; + } + } + + return out; + } + + get() { + return { + columns: this.columns, + rows: this.rows + }; + } + + /** + * Returns the original data which was passed + * based on rowIndex + * @param {Number} rowIndex + * @returns Array|Object + * @memberof DataManager + */ + getData(rowIndex) { + return this.data[rowIndex]; + } + + 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 CellManager { + constructor(instance) { + this.instance = instance; + linkProperties(this, this.instance, [ + 'wrapper', + 'options', + 'style', + 'bodyScrollable', + 'columnmanager', + 'rowmanager', + 'datamanager', + 'keyboard' + ]); + + this.bindEvents(); + } + + bindEvents() { + this.bindFocusCell(); + this.bindEditCell(); + this.bindKeyboardSelection(); + this.bindCopyCellContents(); + this.bindMouseEvents(); + this.bindTreeEvents(); + } + + bindFocusCell() { + this.bindKeyboardNav(); + } + + bindEditCell() { + this.$editingCell = null; + + $.on(this.bodyScrollable, 'dblclick', '.dt-cell', (e, cell) => { + this.activateEditing(cell); + }); + + this.keyboard.on('enter', () => { + 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.inlineFilters) { + this.keyboard.on('ctrl+f', (e) => { + const $cell = $.closest('.dt-cell', e.target); + const { 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', () => { + const noOfCellsCopied = this.copyCellContents(this.$focusedCell, this.$selectionCursor); + const message = `${noOfCellsCopied} cell${noOfCellsCopied > 1 ? 's' : ''} copied`; + if (noOfCellsCopied) { + this.instance.showToastMessage(message, 2); + } + }); + + if (this.options.pasteFromClipboard) { + this.keyboard.on('ctrl+v', (e) => { + // hack + // https://stackoverflow.com/a/2177059/5353542 + this.instance.pasteTarget.focus(); + + setTimeout(() => { + const data = this.instance.pasteTarget.value; + this.instance.pasteTarget.value = ''; + this.pasteContentInCell(data); + }, 10); + + return false; + }); + } + } + + bindMouseEvents() { + let mouseDown = null; + + $.on(this.bodyScrollable, 'mousedown', '.dt-cell', (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', '.dt-cell', throttle$1(selectArea, 50)); + } + + bindTreeEvents() { + $.on(this.bodyScrollable, 'click', '.dt-tree-node__toggle', (e, $toggle) => { + const $cell = $.closest('.dt-cell', $toggle); + const { rowIndex } = $.data($cell); + + if ($cell.classList.contains('dt-cell--tree-close')) { + this.rowmanager.openSingleNode(rowIndex); + } else { + this.rowmanager.closeSingleNode(rowIndex); + } + }); + } + + 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('dt-cell--focus'); + } + + this.$focusedCell = $cell; + $cell.classList.add('dt-cell--focus'); + + // so that keyboard nav works + $cell.focus(); + + this.highlightRowColumnHeader($cell); + } + + highlightRowColumnHeader($cell) { + const { + colIndex, + rowIndex + } = $.data($cell); + + const srNoColIndex = this.datamanager.getColumnIndexById('_rowIndex'); + const colHeaderSelector = `.dt-cell--header-${colIndex}`; + const rowHeaderSelector = `.dt-cell--${srNoColIndex}-${rowIndex}`; + + if (this.lastHeaders) { + this.lastHeaders.forEach(header => header.classList.remove('dt-cell--highlight')); + } + + const colHeader = $(colHeaderSelector, this.wrapper); + const rowHeader = $(rowHeaderSelector, this.wrapper); + + this.lastHeaders = [colHeader, rowHeader]; + this.lastHeaders.forEach(header => header.classList.add('dt-cell--highlight')); + } + + 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(); + this._selectedCells = cells.map(index => this.getCell$(...index)); + requestAnimationFrame(() => { + this._selectedCells.map($cell => $cell.classList.add('dt-cell--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 $cell = this.$focusedCell; + const cells = []; + let colIndex = colIndex1; + let rowIndex = rowIndex1; + const rowIndices = []; + + while (rowIndex <= rowIndex2) { + rowIndices.push(rowIndex); + rowIndex += 1; + } + + rowIndices.map((rowIndex) => { + while (colIndex <= colIndex2) { + cells.push([colIndex, rowIndex]); + colIndex++; + } + colIndex = colIndex1; + }); + + return cells; + } + + clearSelection() { + (this._selectedCells || []) + .forEach($cell => $cell.classList.remove('dt-cell--highlight')); + + this._selectedCells = []; + this.$selectionCursor = null; + } + + getSelectionCursor() { + return this.$selectionCursor || this.$focusedCell; + } + + activateEditing($cell) { + this.focusCell($cell); 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.inlineFilters) { - this.keyboard.on('ctrl+f', (e) => { - const $cell = $.closest('.dt-cell', e.target); - const { 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', () => { - const noOfCellsCopied = this.copyCellContents(this.$focusedCell, this.$selectionCursor); - const message = `${noOfCellsCopied} cell${noOfCellsCopied > 1 ? 's' : ''} copied`; - if (noOfCellsCopied) { - this.instance.showToastMessage(message, 2); - } - }); - } - - bindMouseEvents() { - let mouseDown = null; - - $.on(this.bodyScrollable, 'mousedown', '.dt-cell', (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', '.dt-cell', throttle$1(selectArea, 50)); - } - - bindTreeEvents() { - $.on(this.bodyScrollable, 'click', '.dt-tree-node__toggle', (e, $toggle) => { - const $cell = $.closest('.dt-cell', $toggle); - const { rowIndex } = $.data($cell); - - if ($cell.classList.contains('dt-cell--tree-close')) { - this.rowmanager.openSingleNode(rowIndex); - } else { - this.rowmanager.closeSingleNode(rowIndex); - } - }); - } - - 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('dt-cell--focus'); - } - - this.$focusedCell = $cell; - $cell.classList.add('dt-cell--focus'); - - // so that keyboard nav works - $cell.focus(); - - this.highlightRowColumnHeader($cell); - } - - highlightRowColumnHeader($cell) { - const { - colIndex, - rowIndex - } = $.data($cell); - - const srNoColIndex = this.datamanager.getColumnIndexById('_rowIndex'); - const colHeaderSelector = `.dt-cell--header-${colIndex}`; - const rowHeaderSelector = `.dt-cell--${srNoColIndex}-${rowIndex}`; - - if (this.lastHeaders) { - this.lastHeaders.forEach(header => header.classList.remove('dt-cell--highlight')); - } - - const colHeader = $(colHeaderSelector, this.wrapper); - const rowHeader = $(rowHeaderSelector, this.wrapper); - - this.lastHeaders = [colHeader, rowHeader]; - this.lastHeaders.forEach(header => header.classList.add('dt-cell--highlight')); - } - - 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(); - this._selectedCells = cells.map(index => this.getCell$(...index)); - requestAnimationFrame(() => { - this._selectedCells.map($cell => $cell.classList.add('dt-cell--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; - } - - const cells = []; - let colIndex = colIndex1; - let rowIndex = rowIndex1; - const rowIndices = []; - - while (rowIndex <= rowIndex2) { - rowIndices.push(rowIndex); - rowIndex += 1; - } - - rowIndices.map((rowIndex) => { - while (colIndex <= colIndex2) { - cells.push([colIndex, rowIndex]); - colIndex++; - } - colIndex = colIndex1; - }); - - return cells; - } - - clearSelection() { - (this._selectedCells || []) - .forEach($cell => $cell.classList.remove('dt-cell--highlight')); - - this._selectedCells = []; - 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 + const col = this.columnmanager.getColumn(colIndex); + if (col && (col.editable === false || col.focusable === false)) { return; } - } - this.$editingCell = $cell; - $cell.classList.add('dt-cell--editing'); - - const $editCell = $('.dt-cell__edit', $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('dt-cell--editing'); - this.$editingCell = null; - } - - getEditor(colIndex, rowIndex, value, parent) { - const column = this.datamanager.getColumn(colIndex); - const row = this.datamanager.getRow(rowIndex); - const data = this.datamanager.getData(rowIndex); - let editor = this.options.getEditor ? - this.options.getEditor(colIndex, rowIndex, value, parent, column, row, data) : - this.getDefaultEditor(parent); - - if (editor === false) { - // explicitly returned false - return false; - } - if (editor === undefined) { - // didn't return editor, fallback to default - editor = this.getDefaultEditor(parent); - } - - return editor; - } - - getDefaultEditor(parent) { - const $input = $.create('input', { - class: 'dt-input', - type: 'text', - inside: parent - }); - - return { - initValue(value) { - $input.focus(); - $input.value = value; - }, - getValue() { - return $input.value; - }, - setValue(value) { - $input.value = value; + const cell = this.getCell(colIndex, rowIndex); + if (cell && cell.editable === false) { + return; } - }; - } - submitEditing() { - if (!this.$editingCell) return; - const $cell = this.$editingCell; - const { - rowIndex, - colIndex - } = $.data($cell); - const col = this.datamanager.getColumn(colIndex); + if (this.$editingCell) { + const { + _rowIndex, + _colIndex + } = $.data(this.$editingCell); - 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); - }); + if (rowIndex === _rowIndex && colIndex === _colIndex) { + // editing the same cell + return; } } + + this.$editingCell = $cell; + $cell.classList.add('dt-cell--editing'); + + const $editCell = $('.dt-cell__edit', $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); + } } - this.currentCellEditor = null; - } + 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('dt-cell--editing'); + this.$editingCell = null; + } + + getEditor(colIndex, rowIndex, value, parent) { + const column = this.datamanager.getColumn(colIndex); + const row = this.datamanager.getRow(rowIndex); + const data = this.datamanager.getData(rowIndex); + let editor = this.options.getEditor ? + this.options.getEditor(colIndex, rowIndex, value, parent, column, row, data) : + this.getDefaultEditor(parent); + + if (editor === false) { + // explicitly returned false + return false; + } + if (editor === undefined) { + // didn't return editor, fallback to default + editor = this.getDefaultEditor(parent); + } + + return editor; + } + + getDefaultEditor(parent) { + const $input = $.create('input', { + class: 'dt-input', + 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 1; + } + const cells = this.getCellsInRange($cell1, $cell2); + + if (!cells) return 0; + + const rows = 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; + }, []); + + const values = rows + // join values by tab + .map(row => row.join('\t')) + // join rows by newline + .join('\n'); + + copyTextToClipboard(values); + + // return no of cells copied + return rows.reduce((total, row) => total + row.length, 0); + } + + pasteContentInCell(data) { + if (!this.$focusedCell) return; + + const matrix = data + .split('\n') + .map(row => row.split('\t')) + .filter(row => row.length && row.every(it => it)); + + let { colIndex, rowIndex } = $.data(this.$focusedCell); + + let focusedCell = { + colIndex: +colIndex, + rowIndex: +rowIndex + }; + + matrix.forEach((row, i) => { + let rowIndex = i + focusedCell.rowIndex; + row.forEach((cell, j) => { + let colIndex = j + focusedCell.colIndex; + this.updateCell(colIndex, rowIndex, cell); + }); + }); + } + + 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.selector(cell.colIndex, cell.rowIndex), this.bodyScrollable); + $cell.innerHTML = this.getCellContent(cell); + } + + toggleTreeButton(rowIndex, flag) { + const colIndex = this.columnmanager.getFirstColumnIndex(); + const $cell = this.getCell$(colIndex, rowIndex); + if ($cell) { + $cell.classList[flag ? 'remove' : 'add']('dt-cell--tree-close'); + } + } + + isStandardCell(colIndex) { + // Standard cells are in Sr. No and Checkbox column + return colIndex < this.columnmanager.getFirstColumnIndex(); + } + + getCell$(colIndex, rowIndex) { + return $(this.selector(colIndex, rowIndex), this.bodyScrollable); + } + + getAboveCell$($cell) { + const { + colIndex + } = $.data($cell); + + let $aboveRow = $cell.parentElement.previousElementSibling; + while ($aboveRow && $aboveRow.classList.contains('dt-row--hide')) { + $aboveRow = $aboveRow.previousElementSibling; + } + + if (!$aboveRow) return $cell; + return $(`.dt-cell--col-${colIndex}`, $aboveRow); + } + + getBelowCell$($cell) { + const { + colIndex + } = $.data($cell); + + let $belowRow = $cell.parentElement.nextElementSibling; + while ($belowRow && $belowRow.classList.contains('dt-row--hide')) { + $belowRow = $belowRow.nextElementSibling; + } + + if (!$belowRow) return $cell; + return $(`.dt-cell--col-${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); + } + + getRowHeight() { + return $.style($('.dt-row', this.bodyScrollable), 'height'); + } + + scrollToCell($cell) { + if ($.inViewport($cell, this.bodyScrollable)) return false; - copyCellContents($cell1, $cell2) { - if (!$cell2 && $cell1) { - // copy only focusedCell const { - colIndex, rowIndex - } = $.data($cell1); - const cell = this.getCell(colIndex, rowIndex); - copyTextToClipboard(cell.content); - return 1; - } - const cells = this.getCellsInRange($cell1, $cell2); - - if (!cells) return 0; - - const rows = 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; - }, []); - - const values = rows - // join values by tab - .map(row => row.join('\t')) - // join rows by newline - .join('\n'); - - copyTextToClipboard(values); - - // return no of cells copied - return rows.reduce((total, row) => total + row.length, 0); - } - - 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.selector(cell.colIndex, cell.rowIndex), this.bodyScrollable); - $cell.innerHTML = this.getCellContent(cell); - } - - toggleTreeButton(rowIndex, flag) { - const colIndex = this.columnmanager.getFirstColumnIndex(); - const $cell = this.getCell$(colIndex, rowIndex); - if ($cell) { - $cell.classList[flag ? 'remove' : 'add']('dt-cell--tree-close'); - } - } - - isStandardCell(colIndex) { - // Standard cells are in Sr. No and Checkbox column - return colIndex < this.columnmanager.getFirstColumnIndex(); - } - - getCell$(colIndex, rowIndex) { - return $(this.selector(colIndex, rowIndex), this.bodyScrollable); - } - - getAboveCell$($cell) { - const { - colIndex - } = $.data($cell); - - let $aboveRow = $cell.parentElement.previousElementSibling; - while ($aboveRow && $aboveRow.classList.contains('dt-row--hide')) { - $aboveRow = $aboveRow.previousElementSibling; + } = $.data($cell); + this.rowmanager.scrollToRow(rowIndex); + return false; } - if (!$aboveRow) return $cell; - return $(`.dt-cell--col-${colIndex}`, $aboveRow); - } - - getBelowCell$($cell) { - const { - colIndex - } = $.data($cell); - - let $belowRow = $cell.parentElement.nextElementSibling; - while ($belowRow && $belowRow.classList.contains('dt-row--hide')) { - $belowRow = $belowRow.nextElementSibling; + getRowCountPerPage() { + return Math.ceil(this.instance.getViewportHeight() / this.getRowHeight()); } - if (!$belowRow) return $cell; - return $(`.dt-cell--col-${colIndex}`, $belowRow); - } + getCellHTML(cell) { + const { + rowIndex, + colIndex, + isHeader, + isFilter + } = cell; + const dataAttr = makeDataAttributeString({ + rowIndex, + colIndex, + isHeader, + isFilter + }); - getLeftCell$($cell) { - return $cell.previousElementSibling; - } + const isBodyCell = !(isHeader || isFilter); - getRightCell$($cell) { - return $cell.nextElementSibling; - } + const className = [ + 'dt-cell', + 'dt-cell--col-' + colIndex, + isBodyCell ? `dt-cell--${colIndex}-${rowIndex}` : '', + isBodyCell ? 'dt-cell--row-' + rowIndex : '', + isHeader ? 'dt-cell--header' : '', + isHeader ? `dt-cell--header-${colIndex}` : '', + isFilter ? 'dt-cell--filter' : '' + ].join(' '); - 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); - } - - getRowHeight() { - return $.style($('.dt-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 - }); - - const isBodyCell = !(isHeader || isFilter); - - const className = [ - 'dt-cell', - 'dt-cell--col-' + colIndex, - isBodyCell ? `dt-cell--${colIndex}-${rowIndex}` : '', - isBodyCell ? 'dt-cell--row-' + rowIndex : '', - isHeader ? 'dt-cell--header' : '', - isHeader ? `dt-cell--header-${colIndex}` : '', - isFilter ? 'dt-cell--filter' : '' - ].join(' '); - - return ` + return `