@reactual/handsontable
Version:
Spreadsheet-like data grid editor
1,360 lines (1,151 loc) • 115 kB
JavaScript
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