UNPKG

@reactual/handsontable

Version:

Spreadsheet-like data grid editor

1,360 lines (1,151 loc) 115 kB
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } import numbro from 'numbro'; import { addClass, empty, isChildOfWebComponentTable, removeClass } from './helpers/dom/element'; import { columnFactory } from './helpers/setting'; import { isFunction } from './helpers/function'; import { isDefined, isUndefined, isRegExp, _injectProductInfo } from './helpers/mixed'; import { isMobileBrowser } from './helpers/browser'; import DataMap from './dataMap'; import EditorManager from './editorManager'; import EventManager from './eventManager'; import { deepClone, duckSchema, extend, isObject, isObjectEquals, deepObjectSize, hasOwnProperty, createObjectPropListener } from './helpers/object'; import { arrayFlatten, arrayMap } from './helpers/array'; import { getPlugin } from './plugins'; import { getRenderer } from './renderers'; import { getValidator } from './validators'; import { randomString } from './helpers/string'; import { rangeEach } from './helpers/number'; import TableView from './tableView'; import DataSource from './dataSource'; import { translateRowsToColumns, cellMethodLookupFactory, spreadsheetColumnLabel } from './helpers/data'; import { getTranslator } from './utils/recordTranslator'; import { registerAsRootInstance, hasValidParameter, isRootInstance } from './utils/rootInstance'; import { CellCoords, CellRange, ViewportColumnsCalculator } from './3rdparty/walkontable/src'; import Hooks from './pluginHooks'; import DefaultSettings from './defaultSettings'; import { getCellType } from './cellTypes'; var activeGuid = null; /** * Handsontable constructor * * @core * @dependencies numbro * @constructor Core * @description * * After Handsontable is constructed, you can modify the grid behavior using the available public methods. * * --- * ## How to call methods * * These are 2 equal ways to call a Handsontable method: * * ```js * // all following examples assume that you constructed Handsontable like this * var ht = new Handsontable(document.getElementById('example1'), options); * * // now, to use setDataAtCell method, you can either: * ht.setDataAtCell(0, 0, 'new value'); * ``` * * Alternatively, you can call the method using jQuery wrapper (__obsolete__, requires initialization using our jQuery guide * ```js * $('#example1').handsontable('setDataAtCell', 0, 0, 'new value'); * ``` * --- */ export default function Core(rootElement, userSettings) { var rootInstanceSymbol = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var priv, datamap, dataSource, grid, selection, editorManager, instance = this, GridSettings = function GridSettings() {}, eventManager = new EventManager(instance); extend(GridSettings.prototype, DefaultSettings.prototype); // create grid settings as a copy of default settings extend(GridSettings.prototype, userSettings); // overwrite defaults with user settings extend(GridSettings.prototype, expandType(userSettings)); if (hasValidParameter(rootInstanceSymbol)) { registerAsRootInstance(this); } this.rootElement = rootElement; this.isHotTableEnv = isChildOfWebComponentTable(this.rootElement); EventManager.isHotTableEnv = this.isHotTableEnv; this.container = document.createElement('div'); this.renderCall = false; rootElement.insertBefore(this.container, rootElement.firstChild); if ('ce' !== '\x63\x65' && isRootInstance(this)) { _injectProductInfo(userSettings.licenseKey, rootElement); } this.guid = 'ht_' + randomString(); // this is the namespace for global events var recordTranslator = getTranslator(instance); dataSource = new DataSource(instance); if (!this.rootElement.id || this.rootElement.id.substring(0, 3) === 'ht_') { this.rootElement.id = this.guid; // if root element does not have an id, assign a random id } priv = { cellSettings: [], columnSettings: [], columnsSettingConflicts: ['data', 'width'], settings: new GridSettings(), // current settings instance selRange: null, // exposed by public method `getSelectedRange` isPopulated: null, scrollable: null, firstRun: true }; grid = { /** * Inserts or removes rows and columns * * @memberof Core# * @function alter * @private * @param {String} action Possible values: "insert_row", "insert_col", "remove_row", "remove_col" * @param {Number} index * @param {Number} amount * @param {String} [source] Optional. Source of hook runner. * @param {Boolean} [keepEmptyRows] Optional. Flag for preventing deletion of empty rows. */ alter: function alter(action, index, amount, source, keepEmptyRows) { var delta; amount = amount || 1; function spliceWith(data, index, count, toInject) { var valueFactory = function valueFactory() { var result = void 0; if (toInject === 'array') { result = []; } else if (toInject === 'object') { result = {}; } return result; }; var spliceArgs = arrayMap(new Array(count), function () { return valueFactory(); }); spliceArgs.unshift(index, 0); data.splice.apply(data, _toConsumableArray(spliceArgs)); } /* eslint-disable no-case-declarations */ switch (action) { case 'insert_row': var numberOfSourceRows = instance.countSourceRows(); if (instance.getSettings().maxRows === numberOfSourceRows) { return; } index = isDefined(index) ? index : numberOfSourceRows; delta = datamap.createRow(index, amount, source); spliceWith(priv.cellSettings, index, amount, 'array'); if (delta) { if (selection.isSelected() && priv.selRange.from.row >= index) { priv.selRange.from.row += delta; selection.transformEnd(delta, 0); // will call render() internally } else { selection.refreshBorders(); // it will call render and prepare methods } } break; case 'insert_col': delta = datamap.createCol(index, amount, source); for (var row = 0, len = instance.countSourceRows(); row < len; row++) { if (priv.cellSettings[row]) { spliceWith(priv.cellSettings[row], index, amount); } } if (delta) { if (Array.isArray(instance.getSettings().colHeaders)) { var spliceArray = [index, 0]; spliceArray.length += delta; // inserts empty (undefined) elements at the end of an array Array.prototype.splice.apply(instance.getSettings().colHeaders, spliceArray); // inserts empty (undefined) elements into the colHeader array } if (selection.isSelected() && priv.selRange.from.col >= index) { priv.selRange.from.col += delta; selection.transformEnd(0, delta); // will call render() internally } else { selection.refreshBorders(); // it will call render and prepare methods } } break; case 'remove_row': datamap.removeRow(index, amount, source); priv.cellSettings.splice(index, amount); var totalRows = instance.countRows(); var fixedRowsTop = instance.getSettings().fixedRowsTop; if (fixedRowsTop >= index + 1) { instance.getSettings().fixedRowsTop -= Math.min(amount, fixedRowsTop - index); } var fixedRowsBottom = instance.getSettings().fixedRowsBottom; if (fixedRowsBottom && index >= totalRows - fixedRowsBottom) { instance.getSettings().fixedRowsBottom -= Math.min(amount, fixedRowsBottom); } grid.adjustRowsAndCols(); selection.refreshBorders(); // it will call render and prepare methods break; case 'remove_col': var visualColumnIndex = recordTranslator.toPhysicalColumn(index); datamap.removeCol(index, amount, source); for (var _row = 0, _len = instance.countSourceRows(); _row < _len; _row++) { if (priv.cellSettings[_row]) { // if row hasn't been rendered it wouldn't have cellSettings priv.cellSettings[_row].splice(visualColumnIndex, amount); } } var fixedColumnsLeft = instance.getSettings().fixedColumnsLeft; if (fixedColumnsLeft >= index + 1) { instance.getSettings().fixedColumnsLeft -= Math.min(amount, fixedColumnsLeft - index); } if (Array.isArray(instance.getSettings().colHeaders)) { if (typeof visualColumnIndex === 'undefined') { visualColumnIndex = -1; } instance.getSettings().colHeaders.splice(visualColumnIndex, amount); } grid.adjustRowsAndCols(); selection.refreshBorders(); // it will call render and prepare methods break; default: throw new Error('There is no such action "' + action + '"'); } if (!keepEmptyRows) { grid.adjustRowsAndCols(); // makes sure that we did not add rows that will be removed in next refresh } }, /** * Makes sure there are empty rows at the bottom of the table */ adjustRowsAndCols: function adjustRowsAndCols() { if (priv.settings.minRows) { // should I add empty rows to data source to meet minRows? var rows = instance.countRows(); if (rows < priv.settings.minRows) { for (var r = 0, minRows = priv.settings.minRows; r < minRows - rows; r++) { datamap.createRow(instance.countRows(), 1, 'auto'); } } } if (priv.settings.minSpareRows) { var emptyRows = instance.countEmptyRows(true); // should I add empty rows to meet minSpareRows? if (emptyRows < priv.settings.minSpareRows) { for (; emptyRows < priv.settings.minSpareRows && instance.countSourceRows() < priv.settings.maxRows; emptyRows++) { datamap.createRow(instance.countRows(), 1, 'auto'); } } } { var emptyCols = void 0; // count currently empty cols if (priv.settings.minCols || priv.settings.minSpareCols) { emptyCols = instance.countEmptyCols(true); } // should I add empty cols to meet minCols? if (priv.settings.minCols && !priv.settings.columns && instance.countCols() < priv.settings.minCols) { for (; instance.countCols() < priv.settings.minCols; emptyCols++) { datamap.createCol(instance.countCols(), 1, 'auto'); } } // should I add empty cols to meet minSpareCols? if (priv.settings.minSpareCols && !priv.settings.columns && instance.dataType === 'array' && emptyCols < priv.settings.minSpareCols) { for (; emptyCols < priv.settings.minSpareCols && instance.countCols() < priv.settings.maxCols; emptyCols++) { datamap.createCol(instance.countCols(), 1, 'auto'); } } } var rowCount = instance.countRows(); var colCount = instance.countCols(); if (rowCount === 0 || colCount === 0) { selection.deselect(); } if (selection.isSelected()) { var selectionChanged = false; var fromRow = priv.selRange.from.row; var fromCol = priv.selRange.from.col; var toRow = priv.selRange.to.row; var toCol = priv.selRange.to.col; // if selection is outside, move selection to last row if (fromRow > rowCount - 1) { fromRow = rowCount - 1; selectionChanged = true; if (toRow > fromRow) { toRow = fromRow; } } else if (toRow > rowCount - 1) { toRow = rowCount - 1; selectionChanged = true; if (fromRow > toRow) { fromRow = toRow; } } // if selection is outside, move selection to last row if (fromCol > colCount - 1) { fromCol = colCount - 1; selectionChanged = true; if (toCol > fromCol) { toCol = fromCol; } } else if (toCol > colCount - 1) { toCol = colCount - 1; selectionChanged = true; if (fromCol > toCol) { fromCol = toCol; } } if (selectionChanged) { instance.selectCell(fromRow, fromCol, toRow, toCol); } } if (instance.view) { instance.view.wt.wtOverlays.adjustElementsSize(); } }, /** * Populate the data from the provided 2d array from the given cell coordinates. * * @private * @param {Object} start Start selection position. Visual indexes. * @param {Array} input 2d data array. * @param {Object} [end] End selection position (only for drag-down mode). Visual indexes. * @param {String} [source="populateFromArray"] Source information string. * @param {String} [method="overwrite"] Populate method. Possible options: `shift_down`, `shift_right`, `overwrite`. * @param {String} direction (left|right|up|down) String specifying the direction. * @param {Array} deltas The deltas array. A difference between values of adjacent cells. * Useful **only** when the type of handled cells is `numeric`. * @returns {Object|undefined} ending td in pasted area (only if any cell was changed). */ populateFromArray: function populateFromArray(start, input, end, source, method, direction, deltas) { // TODO: either remove or implement the `direction` argument. Currently it's not working at all. var r, rlen, c, clen, setData = [], current = {}; rlen = input.length; if (rlen === 0) { return false; } var repeatCol, repeatRow, cmax, rmax, baseEnd = { row: end === null ? null : end.row, col: end === null ? null : end.col }; /* eslint-disable no-case-declarations */ // insert data with specified pasteMode method switch (method) { case 'shift_down': repeatCol = end ? end.col - start.col + 1 : 0; repeatRow = end ? end.row - start.row + 1 : 0; input = translateRowsToColumns(input); for (c = 0, clen = input.length, cmax = Math.max(clen, repeatCol); c < cmax; c++) { if (c < clen) { var _instance; for (r = 0, rlen = input[c].length; r < repeatRow - rlen; r++) { input[c].push(input[c][r % rlen]); } input[c].unshift(start.col + c, start.row, 0); (_instance = instance).spliceCol.apply(_instance, _toConsumableArray(input[c])); } else { var _instance2; input[c % clen][0] = start.col + c; (_instance2 = instance).spliceCol.apply(_instance2, _toConsumableArray(input[c % clen])); } } break; case 'shift_right': repeatCol = end ? end.col - start.col + 1 : 0; repeatRow = end ? end.row - start.row + 1 : 0; for (r = 0, rlen = input.length, rmax = Math.max(rlen, repeatRow); r < rmax; r++) { if (r < rlen) { var _instance3; for (c = 0, clen = input[r].length; c < repeatCol - clen; c++) { input[r].push(input[r][c % clen]); } input[r].unshift(start.row + r, start.col, 0); (_instance3 = instance).spliceRow.apply(_instance3, _toConsumableArray(input[r])); } else { var _instance4; input[r % rlen][0] = start.row + r; (_instance4 = instance).spliceRow.apply(_instance4, _toConsumableArray(input[r % rlen])); } } break; case 'overwrite': default: // overwrite and other not specified options current.row = start.row; current.col = start.col; var selected = { // selected range row: end && start ? end.row - start.row + 1 : 1, col: end && start ? end.col - start.col + 1 : 1 }; var skippedRow = 0; var skippedColumn = 0; var pushData = true; var cellMeta = void 0; var getInputValue = function getInputValue(row) { var col = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var rowValue = input[row % input.length]; if (col !== null) { return rowValue[col % rowValue.length]; } return rowValue; }; var rowInputLength = input.length; var rowSelectionLength = end ? end.row - start.row + 1 : 0; if (end) { rlen = rowSelectionLength; } else { rlen = Math.max(rowInputLength, rowSelectionLength); } for (r = 0; r < rlen; r++) { if (end && current.row > end.row && rowSelectionLength > rowInputLength || !priv.settings.allowInsertRow && current.row > instance.countRows() - 1 || current.row >= priv.settings.maxRows) { break; } var visualRow = r - skippedRow; var colInputLength = getInputValue(visualRow).length; var colSelectionLength = end ? end.col - start.col + 1 : 0; if (end) { clen = colSelectionLength; } else { clen = Math.max(colInputLength, colSelectionLength); } current.col = start.col; cellMeta = instance.getCellMeta(current.row, current.col); if ((source === 'CopyPaste.paste' || source === 'Autofill.autofill') && cellMeta.skipRowOnPaste) { skippedRow++; current.row++; rlen++; /* eslint-disable no-continue */ continue; } skippedColumn = 0; for (c = 0; c < clen; c++) { if (end && current.col > end.col && colSelectionLength > colInputLength || !priv.settings.allowInsertColumn && current.col > instance.countCols() - 1 || current.col >= priv.settings.maxCols) { break; } cellMeta = instance.getCellMeta(current.row, current.col); if ((source === 'CopyPaste.paste' || source === 'Autofill.fill') && cellMeta.skipColumnOnPaste) { skippedColumn++; current.col++; clen++; continue; } if (cellMeta.readOnly) { current.col++; /* eslint-disable no-continue */ continue; } var visualColumn = c - skippedColumn; var value = getInputValue(visualRow, visualColumn); var orgValue = instance.getDataAtCell(current.row, current.col); var index = { row: visualRow, col: visualColumn }; if (source === 'Autofill.fill') { var result = instance.runHooks('beforeAutofillInsidePopulate', index, direction, input, deltas, {}, selected); if (result) { value = isUndefined(result.value) ? value : result.value; } } if (value !== null && (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object') { if (orgValue === null || (typeof orgValue === 'undefined' ? 'undefined' : _typeof(orgValue)) !== 'object') { pushData = false; } else { var orgValueSchema = duckSchema(orgValue[0] || orgValue); var valueSchema = duckSchema(value[0] || value); /* eslint-disable max-depth */ if (isObjectEquals(orgValueSchema, valueSchema)) { value = deepClone(value); } else { pushData = false; } } } else if (orgValue !== null && (typeof orgValue === 'undefined' ? 'undefined' : _typeof(orgValue)) === 'object') { pushData = false; } if (pushData) { setData.push([current.row, current.col, value]); } pushData = true; current.col++; } current.row++; } instance.setDataAtCell(setData, null, null, source || 'populateFromArray'); break; } } }; /* eslint-disable no-multi-assign */ this.selection = selection = { // this public assignment is only temporary inProgress: false, selectedHeader: { cols: false, rows: false }, /** * @param {Boolean} [rows=false] * @param {Boolean} [cols=false] * @param {Boolean} [corner=false] */ setSelectedHeaders: function setSelectedHeaders() { var rows = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; var cols = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var corner = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; instance.selection.selectedHeader.rows = rows; instance.selection.selectedHeader.cols = cols; instance.selection.selectedHeader.corner = corner; }, /** * Sets inProgress to `true`. This enables onSelectionEnd and onSelectionEndByProp to function as desired. */ begin: function begin() { instance.selection.inProgress = true; }, /** * Sets inProgress to `false`. Triggers onSelectionEnd and onSelectionEndByProp. */ finish: function finish() { var sel = instance.getSelected(); instance.runHooks('afterSelectionEnd', sel[0], sel[1], sel[2], sel[3]); instance.runHooks('afterSelectionEndByProp', sel[0], instance.colToProp(sel[1]), sel[2], instance.colToProp(sel[3])); instance.selection.inProgress = false; }, /** * @returns {Boolean} */ isInProgress: function isInProgress() { return instance.selection.inProgress; }, /** * Starts selection range on given td object. * * @param {CellCoords} coords Visual coords. * @param keepEditorOpened */ setRangeStart: function setRangeStart(coords, keepEditorOpened) { instance.runHooks('beforeSetRangeStart', coords); priv.selRange = new CellRange(coords, coords, coords); selection.setRangeEnd(coords, null, keepEditorOpened); }, /** * Starts selection range on given td object. * * @param {CellCoords} coords Visual coords. * @param keepEditorOpened */ setRangeStartOnly: function setRangeStartOnly(coords) { instance.runHooks('beforeSetRangeStartOnly', coords); priv.selRange = new CellRange(coords, coords, coords); }, /** * Ends selection range on given td object. * * @param {CellCoords} coords Visual coords. * @param {Boolean} [scrollToCell=true] If `true`, viewport will be scrolled to range end * @param {Boolean} [keepEditorOpened] If `true`, cell editor will be still opened after changing selection range */ setRangeEnd: function setRangeEnd(coords, scrollToCell, keepEditorOpened) { if (priv.selRange === null) { return; } var disableVisualSelection, isHeaderSelected = false, areCoordsPositive = true; var firstVisibleRow = instance.view.wt.wtTable.getFirstVisibleRow(); var firstVisibleColumn = instance.view.wt.wtTable.getFirstVisibleColumn(); var newRangeCoords = { row: null, col: null }; // trigger handlers instance.runHooks('beforeSetRangeEnd', coords); instance.selection.begin(); newRangeCoords.row = coords.row < 0 ? firstVisibleRow : coords.row; newRangeCoords.col = coords.col < 0 ? firstVisibleColumn : coords.col; priv.selRange.to = new CellCoords(newRangeCoords.row, newRangeCoords.col); if (!priv.settings.multiSelect) { priv.selRange.from = coords; } // set up current selection instance.view.wt.selections.current.clear(); disableVisualSelection = instance.getCellMeta(priv.selRange.highlight.row, priv.selRange.highlight.col).disableVisualSelection; if (typeof disableVisualSelection === 'string') { disableVisualSelection = [disableVisualSelection]; } if (disableVisualSelection === false || Array.isArray(disableVisualSelection) && disableVisualSelection.indexOf('current') === -1) { instance.view.wt.selections.current.add(priv.selRange.highlight); } // set up area selection instance.view.wt.selections.area.clear(); if ((disableVisualSelection === false || Array.isArray(disableVisualSelection) && disableVisualSelection.indexOf('area') === -1) && selection.isMultiple()) { instance.view.wt.selections.area.add(priv.selRange.from); instance.view.wt.selections.area.add(priv.selRange.to); } // set up highlight if (priv.settings.currentHeaderClassName || priv.settings.currentRowClassName || priv.settings.currentColClassName) { instance.view.wt.selections.highlight.clear(); instance.view.wt.selections.highlight.add(priv.selRange.from); instance.view.wt.selections.highlight.add(priv.selRange.to); } var preventScrolling = createObjectPropListener('value'); // trigger handlers instance.runHooks('afterSelection', priv.selRange.from.row, priv.selRange.from.col, priv.selRange.to.row, priv.selRange.to.col, preventScrolling); instance.runHooks('afterSelectionByProp', priv.selRange.from.row, datamap.colToProp(priv.selRange.from.col), priv.selRange.to.row, datamap.colToProp(priv.selRange.to.col), preventScrolling); if (priv.selRange.from.row === 0 && priv.selRange.to.row === instance.countRows() - 1 && instance.countRows() > 1 || priv.selRange.from.col === 0 && priv.selRange.to.col === instance.countCols() - 1 && instance.countCols() > 1) { isHeaderSelected = true; } if (coords.row < 0 || coords.col < 0) { areCoordsPositive = false; } if (preventScrolling.isTouched()) { scrollToCell = !preventScrolling.value; } if (scrollToCell !== false && !isHeaderSelected && areCoordsPositive) { if (priv.selRange.from && !selection.isMultiple()) { instance.view.scrollViewport(priv.selRange.from); } else { instance.view.scrollViewport(coords); } } if (selection.selectedHeader.rows && selection.selectedHeader.cols) { addClass(instance.rootElement, ['ht__selection--rows', 'ht__selection--columns']); } else if (selection.selectedHeader.rows) { removeClass(instance.rootElement, 'ht__selection--columns'); addClass(instance.rootElement, 'ht__selection--rows'); } else if (selection.selectedHeader.cols) { removeClass(instance.rootElement, 'ht__selection--rows'); addClass(instance.rootElement, 'ht__selection--columns'); } else { removeClass(instance.rootElement, ['ht__selection--rows', 'ht__selection--columns']); } selection.refreshBorders(null, keepEditorOpened); }, /** * Destroys editor, redraws borders around cells, prepares editor. * * @param {Boolean} [revertOriginal] * @param {Boolean} [keepEditor] */ refreshBorders: function refreshBorders(revertOriginal, keepEditor) { if (!keepEditor) { editorManager.destroyEditor(revertOriginal); } instance.view.render(); if (selection.isSelected() && !keepEditor) { editorManager.prepareEditor(); } }, /** * Returns information if we have a multiselection. * * @returns {Boolean} */ isMultiple: function isMultiple() { var isMultiple = !(priv.selRange.to.col === priv.selRange.from.col && priv.selRange.to.row === priv.selRange.from.row), modifier = instance.runHooks('afterIsMultipleSelection', isMultiple); if (isMultiple) { return modifier; } }, /** * Selects cell relative to current cell (if possible). */ transformStart: function transformStart(rowDelta, colDelta, force, keepEditorOpened) { var delta = new CellCoords(rowDelta, colDelta), rowTransformDir = 0, colTransformDir = 0, totalRows, totalCols, coords, fixedRowsBottom; instance.runHooks('modifyTransformStart', delta); totalRows = instance.countRows(); totalCols = instance.countCols(); fixedRowsBottom = instance.getSettings().fixedRowsBottom; if (priv.selRange.highlight.row + rowDelta > totalRows - 1) { if (force && priv.settings.minSpareRows > 0 && !(fixedRowsBottom && priv.selRange.highlight.row >= totalRows - fixedRowsBottom - 1)) { instance.alter('insert_row', totalRows); totalRows = instance.countRows(); } else if (priv.settings.autoWrapCol) { delta.row = 1 - totalRows; delta.col = priv.selRange.highlight.col + delta.col == totalCols - 1 ? 1 - totalCols : 1; } } else if (priv.settings.autoWrapCol && priv.selRange.highlight.row + delta.row < 0 && priv.selRange.highlight.col + delta.col >= 0) { delta.row = totalRows - 1; delta.col = priv.selRange.highlight.col + delta.col == 0 ? totalCols - 1 : -1; } if (priv.selRange.highlight.col + delta.col > totalCols - 1) { if (force && priv.settings.minSpareCols > 0) { instance.alter('insert_col', totalCols); totalCols = instance.countCols(); } else if (priv.settings.autoWrapRow) { delta.row = priv.selRange.highlight.row + delta.row == totalRows - 1 ? 1 - totalRows : 1; delta.col = 1 - totalCols; } } else if (priv.settings.autoWrapRow && priv.selRange.highlight.col + delta.col < 0 && priv.selRange.highlight.row + delta.row >= 0) { delta.row = priv.selRange.highlight.row + delta.row == 0 ? totalRows - 1 : -1; delta.col = totalCols - 1; } coords = new CellCoords(priv.selRange.highlight.row + delta.row, priv.selRange.highlight.col + delta.col); if (coords.row < 0) { rowTransformDir = -1; coords.row = 0; } else if (coords.row > 0 && coords.row >= totalRows) { rowTransformDir = 1; coords.row = totalRows - 1; } if (coords.col < 0) { colTransformDir = -1; coords.col = 0; } else if (coords.col > 0 && coords.col >= totalCols) { colTransformDir = 1; coords.col = totalCols - 1; } instance.runHooks('afterModifyTransformStart', coords, rowTransformDir, colTransformDir); selection.setRangeStart(coords, keepEditorOpened); }, /** * Sets selection end cell relative to current selection end cell (if possible). */ transformEnd: function transformEnd(rowDelta, colDelta) { var delta = new CellCoords(rowDelta, colDelta), rowTransformDir = 0, colTransformDir = 0, totalRows, totalCols, coords; instance.runHooks('modifyTransformEnd', delta); totalRows = instance.countRows(); totalCols = instance.countCols(); coords = new CellCoords(priv.selRange.to.row + delta.row, priv.selRange.to.col + delta.col); if (coords.row < 0) { rowTransformDir = -1; coords.row = 0; } else if (coords.row > 0 && coords.row >= totalRows) { rowTransformDir = 1; coords.row = totalRows - 1; } if (coords.col < 0) { colTransformDir = -1; coords.col = 0; } else if (coords.col > 0 && coords.col >= totalCols) { colTransformDir = 1; coords.col = totalCols - 1; } instance.runHooks('afterModifyTransformEnd', coords, rowTransformDir, colTransformDir); selection.setRangeEnd(coords, true); }, /** * Returns `true` if currently there is a selection on screen, `false` otherwise. * * @returns {Boolean} */ isSelected: function isSelected() { return priv.selRange !== null; }, /** * Returns `true` if coords is within current selection coords. * * @param {CellCoords} coords * @returns {Boolean} */ inInSelection: function inInSelection(coords) { if (!selection.isSelected()) { return false; } return priv.selRange.includes(coords); }, /** * Deselects all selected cells */ deselect: function deselect() { if (!selection.isSelected()) { return; } instance.selection.inProgress = false; // needed by HT inception priv.selRange = null; instance.view.wt.selections.current.clear(); instance.view.wt.selections.area.clear(); if (priv.settings.currentHeaderClassName || priv.settings.currentRowClassName || priv.settings.currentColClassName) { instance.view.wt.selections.highlight.clear(); } editorManager.destroyEditor(); selection.refreshBorders(); removeClass(instance.rootElement, ['ht__selection--rows', 'ht__selection--columns']); instance.runHooks('afterDeselect'); }, /** * Select all cells */ selectAll: function selectAll() { if (!priv.settings.multiSelect) { return; } selection.setSelectedHeaders(true, true, true); selection.setRangeStart(new CellCoords(0, 0)); selection.setRangeEnd(new CellCoords(instance.countRows() - 1, instance.countCols() - 1), false); }, /** * Deletes data from selected cells */ empty: function empty() { if (!selection.isSelected()) { return; } var topLeft = priv.selRange.getTopLeftCorner(); var bottomRight = priv.selRange.getBottomRightCorner(); var r, c, changes = []; for (r = topLeft.row; r <= bottomRight.row; r++) { for (c = topLeft.col; c <= bottomRight.col; c++) { if (!instance.getCellMeta(r, c).readOnly) { changes.push([r, c, '']); } } } instance.setDataAtCell(changes); } }; this.init = function () { dataSource.setData(priv.settings.data); instance.runHooks('beforeInit'); if (isMobileBrowser()) { addClass(instance.rootElement, 'mobile'); } this.updateSettings(priv.settings, true); this.view = new TableView(this); editorManager = new EditorManager(instance, priv, selection, datamap); this.forceFullRender = true; // used when data was changed instance.runHooks('init'); this.view.render(); if (_typeof(priv.firstRun) === 'object') { instance.runHooks('afterChange', priv.firstRun[0], priv.firstRun[1]); priv.firstRun = false; } instance.runHooks('afterInit'); }; function ValidatorsQueue() { // moved this one level up so it can be used in any function here. Probably this should be moved to a separate file var resolved = false; return { validatorsInQueue: 0, valid: true, addValidatorToQueue: function addValidatorToQueue() { this.validatorsInQueue++; resolved = false; }, removeValidatorFormQueue: function removeValidatorFormQueue() { this.validatorsInQueue = this.validatorsInQueue - 1 < 0 ? 0 : this.validatorsInQueue - 1; this.checkIfQueueIsEmpty(); }, onQueueEmpty: function onQueueEmpty(valid) {}, checkIfQueueIsEmpty: function checkIfQueueIsEmpty() { if (this.validatorsInQueue == 0 && resolved == false) { resolved = true; this.onQueueEmpty(this.valid); } } }; } function validateChanges(changes, source, callback) { var waitingForValidator = new ValidatorsQueue(); waitingForValidator.onQueueEmpty = resolve; for (var i = changes.length - 1; i >= 0; i--) { if (changes[i] === null) { changes.splice(i, 1); } else { var row = changes[i][0]; var col = datamap.propToCol(changes[i][1]); var cellProperties = instance.getCellMeta(row, col); if (cellProperties.type === 'numeric' && typeof changes[i][3] === 'string') { if (changes[i][3].length > 0 && (/^-?[\d\s]*(\.|,)?\d*$/.test(changes[i][3]) || cellProperties.format)) { var len = changes[i][3].length; if (isUndefined(cellProperties.language)) { numbro.culture('en-US'); } else if (changes[i][3].indexOf('.') === len - 3 && changes[i][3].indexOf(',') === -1) { // this input in format XXXX.XX is likely to come from paste. Let's parse it using international rules numbro.culture('en-US'); } else { numbro.culture(cellProperties.language); } var _numbro$cultureData = numbro.cultureData(numbro.culture()), delimiters = _numbro$cultureData.delimiters; // try to parse to float - https://github.com/foretagsplatsen/numbro/pull/183 if (numbro.validate(changes[i][3]) && !isNaN(changes[i][3])) { changes[i][3] = parseFloat(changes[i][3]); } else { changes[i][3] = numbro().unformat(changes[i][3]) || changes[i][3]; } } } /* eslint-disable no-loop-func */ if (instance.getCellValidator(cellProperties)) { waitingForValidator.addValidatorToQueue(); instance.validateCell(changes[i][3], cellProperties, function (i, cellProperties) { return function (result) { if (typeof result !== 'boolean') { throw new Error('Validation error: result is not boolean'); } if (result === false && cellProperties.allowInvalid === false) { changes.splice(i, 1); // cancel the change cellProperties.valid = true; // we cancelled the change, so cell value is still valid var cell = instance.getCell(cellProperties.visualRow, cellProperties.visualCol); removeClass(cell, instance.getSettings().invalidCellClassName); --i; } waitingForValidator.removeValidatorFormQueue(); }; }(i, cellProperties), source); } } } waitingForValidator.checkIfQueueIsEmpty(); function resolve() { var beforeChangeResult; if (changes.length) { beforeChangeResult = instance.runHooks('beforeChange', changes, source); if (isFunction(beforeChangeResult)) { console.warn('Your beforeChange callback returns a function. It\'s not supported since Handsontable 0.12.1 (and the returned function will not be executed).'); } else if (beforeChangeResult === false) { changes.splice(0, changes.length); // invalidate all changes (remove everything from array) } } callback(); // called when async validators are resolved and beforeChange was not async } } /** * Internal function to apply changes. Called after validateChanges * * @private * @param {Array} changes Array in form of [row, prop, oldValue, newValue] * @param {String} source String that identifies how this change will be described in changes array (useful in onChange callback) * @fires Hooks#beforeChangeRender * @fires Hooks#afterChange */ function applyChanges(changes, source) { var i = changes.length - 1; if (i < 0) { return; } for (; i >= 0; i--) { var skipThisChange = false; if (changes[i] === null) { changes.splice(i, 1); /* eslint-disable no-continue */ continue; } if (changes[i][2] == null && changes[i][3] == null) { /* eslint-disable no-continue */ continue; } if (priv.settings.allowInsertRow) { while (changes[i][0] > instance.countRows() - 1) { var numberOfCreatedRows = datamap.createRow(void 0, void 0, source); if (numberOfCreatedRows === 0) { skipThisChange = true; break; } } } if (skipThisChange) { /* eslint-disable no-continue */ continue; } if (instance.dataType === 'array' && (!priv.settings.columns || priv.settings.columns.length === 0) && priv.settings.allowInsertColumn) { while (datamap.propToCol(changes[i][1]) > instance.countCols() - 1) { datamap.createCol(void 0, void 0, source); } } datamap.set(changes[i][0], changes[i][1], changes[i][3]); } instance.forceFullRender = true; // used when data was changed grid.adjustRowsAndCols(); instance.runHooks('beforeChangeRender', changes, source); selection.refreshBorders(null, true); instance.view.wt.wtOverlays.adjustElementsSize(); instance.runHooks('afterChange', changes, source || 'edit'); var activeEditor = instance.getActiveEditor(); if (activeEditor && isDefined(activeEditor.refreshValue)) { activeEditor.refreshValue(); } } this.validateCell = function (value, cellProperties, callback, source) { var validator = instance.getCellValidator(cellProperties); function done(valid) { var col = cellProperties.visualCol, row = cellProperties.visualRow, td = instance.getCell(row, col, true); if (td && td.nodeName != 'TH') { instance.view.wt.wtSettings.settings.cellRenderer(row, col, td); } callback(valid); } if (isRegExp(validator)) { validator = function (validator) { return function (value, callback) { callback(validator.test(value)); }; }(validator); } if (isFunction(validator)) { value = instance.runHooks('beforeValidate', value, cellProperties.visualRow, cellProperties.prop, source); // To provide consistent behaviour, validation should be always asynchronous instance._registerTimeout(setTimeout(function () { validator.call(cellProperties, value, function (valid) { valid = instance.runHooks('afterValidate', valid, value, cellProperties.visualRow, cellProperties.prop, source); cellProperties.valid = valid; done(valid); instance.runHooks('postAfterValidate', valid, value, cellProperties.visualRow, cellProperties.prop, source); }); }, 0)); } else { // resolve callback even if validator function was not found instance._registerTimeout(setTimeout(function () { cellProperties.valid = true; done(cellProperties.valid); }, 0)); } }; function setDataInputToArray(row, propOrCol, value) { if ((typeof row === 'undefined' ? 'undefined' : _typeof(row)) === 'object') { // is it an array of changes return row; } return [[row, propOrCol, value]]; } /** * @description * Set new value to a cell. To change many cells at once, pass an array of `changes` in format `[[row, col, value], ...]` as * the only parameter. `col` is the index of a __visible__ column (note that if columns were reordered, * the current visible order will be used). `source` is a flag for before/afterChange events. If you pass only array of * changes then `source` could be set as second parameter. * * @memberof Core# * @function setDataAtCell * @param {Number|Array} row Visual row index or array of changes in format `[[row, col, value], ...]`. * @param {Number} col Visual column index. * @param {String} value New value. * @param {String} [source] String that identifies how this change will be described in the changes array (useful in onAfterChange or onBeforeChange callback). */ this.setDataAtCell = function (row, col, value, source) { var input = setDataInputToArray(row, col, value), i, ilen, changes = [], prop; for (i = 0, ilen = input.length; i < ilen; i++) { if (_typeof(input[i]) !== 'object') { throw new Error('Method `setDataAtCell` accepts row number or changes array of arrays as its first parameter'); } if (typeof input[i][1] !== 'number') { throw new Error('Method `setDataAtCell` accepts row and column number as its parameters. If you want to use object property name, use method `setDataAtRowProp`'); } prop = datamap.colToProp(input[i][1]); changes.push([input[i][0], prop, dataSource.getAtCell(recordTranslator.toPhysicalRow(input[i][0]), input[i][1]), input[i][2]]); } if (!source && (typeof row === 'undefined' ? 'undefined' : _typeof(row)) === 'object') { source = col; } instance.runHooks('afterSetDataAtCell', changes, source); validateChanges(changes, source, function () { applyChanges(changes, source); }); }; /** * @description * Set new value to a cell. To change many cells at once, pass an array of `changes` in format `[[row, prop, value], ...]` as * the only parameter. `prop` is the name of the object property (e.g. `first.name`). `source` is a flag for before/afterChange events. * If you pass only array of changes then `source` could be set as second parameter. * * @memberof Core# * @function setDataAtRowProp * @param {Number|Array} row Visual row index or array of changes in format `[[row, prop, value], ...]`. * @param {String} prop Property name or the source string. * @param {String} value Value to be set. * @param {String} [source] String that identifies how this change will be described in changes array (useful in onChange callback). */ this.setDataAtRowProp = function (row, prop, value, source) { var input = setDataInputToArray(row, prop, value), i, ilen, changes = []; for (i = 0, ilen = input.length; i < ilen; i++) { changes.push([input[i][0], input[i][1], dataSource.getAtCell(recordTranslator.toPhysicalRow(input[i][0]), input[i][1]), input[i][2]]); } if (!source && (typeof row === 'undefined' ? 'undefined' : _typeof(row)) === 'object') { source = prop; } instance.runHooks('afterSetDataAtRowProp', changes, source); validateChanges(changes, source, function () { applyChanges(changes, source); }); }; /** * Listen to the keyboard input on document body. * * @memberof Core# * @function listen * @since 0.11 */ this.listen = function () { var invalidActiveElement = !document.activeElement || document.activeElement && document.activeElement.nodeName === void 0; if (document.activeElement && document.activeElement !== document.body && !invalidActiveElement) { document.activeElement.blur(); } else if (invalidActiveElement) { // IE document.body.focus(); } if (instance && !instance.isListening()) { activeGuid = instance.guid; instance.runHooks('afterListen'); } }; /** * Stop listening to keyboard input on the document body. * * @memberof Core# * @function unlisten * @since 0.11 */ this.unlisten = function () { if (this.isListening()) { activeGuid = null; instance.runHooks('afterUnlisten'); } }; /** * Returns `true` if the current Handsontable instance is listening to keyboard input on document body. * * @memberof Core# * @function isListening * @since 0.11 * @returns {Boolean} `true` if the instance is listening, `false` otherwise. */ this.isListening = function () { return activeGuid === instance.guid; }; /** * Destroys the current editor, renders and selects the current cell. * * @memberof Core# * @function destroyEditor * @param {Boolean} [revertOriginal] If != `true`, edited data is saved. Otherwise the previous value is restored. */ this.destroyEditor = function (revertOriginal) { selection.refreshBorders(revertOriginal); }; /** * Populate cells at position with 2D input array (e.g. `[[1, 2], [3, 4]]`). * Use `endRow`, `endCol` when you want to cut input when a certain row is reached. * Optional `source` parameter (default value "populateFromArray") is used to identify this call in the resulting events (beforeChange, afterChange). * Optional `populateMethod` parameter (default value "overwri