/files/jquery.handsontable/0.9.6/jquery.handsontable.js
JavaScript | 1482 lines | 1106 code | 156 blank | 220 comment | 297 complexity | 658e0916b71fae4df77c4f29f0be8648 MD5 | raw file
- /**
- * Handsontable 0.9.6
- * Handsontable is a simple jQuery plugin for editable tables with basic copy-paste compatibility with Excel and Google Docs
- *
- * Copyright 2012, Marcin Warpechowski
- * Licensed under the MIT license.
- * http://handsontable.com/
- *
- * Date: Tue Jun 18 2013 18:56:33 GMT+0200 (Central European Daylight Time)
- */
- /*jslint white: true, browser: true, plusplus: true, indent: 4, maxerr: 50 */
- var Handsontable = { //class namespace
- extension: {}, //extenstion namespace
- helper: {} //helper namespace
- };
- (function ($, window, Handsontable) {
- "use strict";
- /**
- * Handsontable constructor
- * @param rootElement The jQuery element in which Handsontable DOM will be inserted
- * @param userSettings
- * @constructor
- */
- Handsontable.Core = function (rootElement, userSettings) {
- var priv
- , datamap
- , grid
- , selection
- , editproxy
- , autofill
- , instance = this
- , GridSettings = function () {
- };
- Handsontable.helper.inherit(GridSettings, DefaultSettings); //create grid settings as a copy of default settings
- Handsontable.helper.extend(GridSettings.prototype, userSettings); //overwrite defaults with user settings
- this.rootElement = rootElement;
- this.guid = 'ht_' + Handsontable.helper.randomString(); //this is the namespace for global events
- if (!this.rootElement[0].id) {
- this.rootElement[0].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
- settingsFromDOM: {},
- selStart: new Handsontable.SelectionPoint(),
- selEnd: new Handsontable.SelectionPoint(),
- editProxy: false,
- isPopulated: null,
- scrollable: null,
- undoRedo: null,
- extensions: {},
- colToProp: null,
- propToCol: null,
- dataSchema: null,
- dataType: 'array',
- firstRun: true
- };
- datamap = {
- recursiveDuckSchema: function (obj) {
- var schema;
- if ($.isPlainObject(obj)) {
- schema = {};
- for (var i in obj) {
- if (obj.hasOwnProperty(i)) {
- if ($.isPlainObject(obj[i])) {
- schema[i] = datamap.recursiveDuckSchema(obj[i]);
- }
- else {
- schema[i] = null;
- }
- }
- }
- }
- else {
- schema = [];
- }
- return schema;
- },
- recursiveDuckColumns: function (schema, lastCol, parent) {
- var prop, i;
- if (typeof lastCol === 'undefined') {
- lastCol = 0;
- parent = '';
- }
- if ($.isPlainObject(schema)) {
- for (i in schema) {
- if (schema.hasOwnProperty(i)) {
- if (schema[i] === null) {
- prop = parent + i;
- priv.colToProp.push(prop);
- priv.propToCol[prop] = lastCol;
- lastCol++;
- }
- else {
- lastCol = datamap.recursiveDuckColumns(schema[i], lastCol, i + '.');
- }
- }
- }
- }
- return lastCol;
- },
- createMap: function () {
- if (typeof datamap.getSchema() === "undefined") {
- throw new Error("trying to create `columns` definition but you didnt' provide `schema` nor `data`");
- }
- var i, ilen, schema = datamap.getSchema();
- priv.colToProp = [];
- priv.propToCol = {};
- if (priv.settings.columns) {
- for (i = 0, ilen = priv.settings.columns.length; i < ilen; i++) {
- priv.colToProp[i] = priv.settings.columns[i].data;
- priv.propToCol[priv.settings.columns[i].data] = i;
- }
- }
- else {
- datamap.recursiveDuckColumns(schema);
- }
- },
- colToProp: function (col) {
- col = Handsontable.PluginHooks.execute(instance, 'modifyCol', col);
- if (priv.colToProp && typeof priv.colToProp[col] !== 'undefined') {
- return priv.colToProp[col];
- }
- else {
- return col;
- }
- },
- propToCol: function (prop) {
- var col;
- if (typeof priv.propToCol[prop] !== 'undefined') {
- col = priv.propToCol[prop];
- }
- else {
- col = prop;
- }
- col = Handsontable.PluginHooks.execute(instance, 'modifyCol', col);
- return col;
- },
- getSchema: function () {
- if (priv.settings.dataSchema) {
- if (typeof priv.settings.dataSchema === 'function') {
- return priv.settings.dataSchema();
- }
- return priv.settings.dataSchema;
- }
- return priv.duckDataSchema;
- },
- /**
- * Creates row at the bottom of the data array
- * @param {Number} [index] Optional. Index of the row before which the new row will be inserted
- */
- createRow: function (index) {
- var row
- , rowCount = instance.countRows();
- if (typeof index !== 'number' || index >= rowCount) {
- index = rowCount;
- }
- if (priv.dataType === 'array') {
- row = [];
- for (var c = 0, clen = instance.countCols(); c < clen; c++) {
- row.push(null);
- }
- }
- else if (priv.dataType === 'function') {
- row = priv.settings.dataSchema(index);
- }
- else {
- row = $.extend(true, {}, datamap.getSchema());
- }
- if (index === rowCount) {
- GridSettings.prototype.data.push(row);
- }
- else {
- GridSettings.prototype.data.splice(index, 0, row);
- }
- instance.PluginHooks.run('afterCreateRow', index);
- instance.forceFullRender = true; //used when data was changed
- },
- /**
- * Creates col at the right of the data array
- * @param {Object} [index] Optional. Index of the column before which the new column will be inserted
- */
- createCol: function (index) {
- if (priv.dataType === 'object' || priv.settings.columns) {
- throw new Error("Cannot create new column. When data source in an object, you can only have as much columns as defined in first data row, data schema or in the 'columns' setting");
- }
- var r = 0, rlen = instance.countRows()
- , data = GridSettings.prototype.data
- , constructor = Handsontable.helper.columnFactory(GridSettings, priv.columnsSettingConflicts, Handsontable.TextCell);
- if (typeof index !== 'number' || index >= instance.countCols()) {
- for (; r < rlen; r++) {
- if (typeof data[r] === 'undefined') {
- data[r] = [];
- }
- data[r].push(null);
- }
- // Add new column constructor
- priv.columnSettings.push(constructor);
- }
- else {
- for (; r < rlen; r++) {
- data[r].splice(index, 0, null);
- }
- // Add new column constructor at given index
- priv.columnSettings.splice(index, 0, constructor);
- }
- instance.PluginHooks.run('afterCreateCol', index);
- instance.forceFullRender = true; //used when data was changed
- },
- /**
- * Removes row from the data array
- * @param {Number} [index] Optional. Index of the row to be removed. If not provided, the last row will be removed
- * @param {Number} [amount] Optional. Amount of the rows to be removed. If not provided, one row will be removed
- */
- removeRow: function (index, amount) {
- if (!amount) {
- amount = 1;
- }
- if (typeof index !== 'number') {
- index = -amount;
- }
- GridSettings.prototype.data.splice(index, amount);
- instance.PluginHooks.run('afterRemoveRow', index, amount);
- instance.forceFullRender = true; //used when data was changed
- },
- /**
- * Removes column from the data array
- * @param {Number} [index] Optional. Index of the column to be removed. If not provided, the last column will be removed
- * @param {Number} [amount] Optional. Amount of the columns to be removed. If not provided, one column will be removed
- */
- removeCol: function (index, amount) {
- if (priv.dataType === 'object' || priv.settings.columns) {
- throw new Error("cannot remove column with object data source or columns option specified");
- }
- if (!amount) {
- amount = 1;
- }
- if (typeof index !== 'number') {
- index = -amount;
- }
- var data = GridSettings.prototype.data;
- for (var r = 0, rlen = instance.countRows(); r < rlen; r++) {
- data[r].splice(index, amount);
- }
- instance.PluginHooks.run('afterRemoveCol', index, amount);
- priv.columnSettings.splice(index, amount);
- instance.forceFullRender = true; //used when data was changed
- },
- /**
- * Add / removes data from the column
- * @param {Number} col Index of column in which do you want to do splice.
- * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end
- * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed
- * param {...*} elements Optional. The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array
- */
- spliceCol: function (col, index, amount/*, elements...*/) {
- var elements = 4 <= arguments.length ? [].slice.call(arguments, 3) : [];
- var colData = instance.getDataAtCol(col);
- var removed = colData.slice(index, index + amount);
- var after = colData.slice(index + amount);
- Handsontable.helper.extendArray(elements, after);
- var i = 0;
- while (i < amount) {
- elements.push(null); //add null in place of removed elements
- i++;
- }
- Handsontable.helper.to2dArray(elements);
- instance.populateFromArray(index, col, elements, null, null, 'spliceCol');
- return removed;
- },
- /**
- * Add / removes data from the row
- * @param {Number} row Index of row in which do you want to do splice.
- * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end
- * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed
- * param {...*} elements Optional. The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array
- */
- spliceRow: function (row, index, amount/*, elements...*/) {
- var elements = 4 <= arguments.length ? [].slice.call(arguments, 3) : [];
- var rowData = instance.getDataAtRow(row);
- var removed = rowData.slice(index, index + amount);
- var after = rowData.slice(index + amount);
- Handsontable.helper.extendArray(elements, after);
- var i = 0;
- while (i < amount) {
- elements.push(null); //add null in place of removed elements
- i++;
- }
- instance.populateFromArray(row, index, [elements], null, null, 'spliceRow');
- return removed;
- },
- /**
- * Returns single value from the data array
- * @param {Number} row
- * @param {Number} prop
- */
- getVars: {},
- get: function (row, prop) {
- datamap.getVars.row = row;
- datamap.getVars.prop = prop;
- instance.PluginHooks.run('beforeGet', datamap.getVars);
- if (typeof datamap.getVars.prop === 'string' && datamap.getVars.prop.indexOf('.') > -1) {
- var sliced = datamap.getVars.prop.split(".");
- var out = priv.settings.data[datamap.getVars.row];
- if (!out) {
- return null;
- }
- for (var i = 0, ilen = sliced.length; i < ilen; i++) {
- out = out[sliced[i]];
- if (typeof out === 'undefined') {
- return null;
- }
- }
- return out;
- }
- else if (typeof datamap.getVars.prop === 'function') {
- /**
- * allows for interacting with complex structures, for example
- * d3/jQuery getter/setter properties:
- *
- * {columns: [{
- * data: function(row, value){
- * if(arguments.length === 1){
- * return row.property();
- * }
- * row.property(value);
- * }
- * }]}
- */
- return datamap.getVars.prop(priv.settings.data.slice(
- datamap.getVars.row,
- datamap.getVars.row + 1
- )[0]);
- }
- else {
- return priv.settings.data[datamap.getVars.row] ? priv.settings.data[datamap.getVars.row][datamap.getVars.prop] : null;
- }
- },
- /**
- * Saves single value to the data array
- * @param {Number} row
- * @param {Number} prop
- * @param {String} value
- * @param {String} [source] Optional. Source of hook runner.
- */
- setVars: {},
- set: function (row, prop, value, source) {
- datamap.setVars.row = row;
- datamap.setVars.prop = prop;
- datamap.setVars.value = value;
- instance.PluginHooks.run('beforeSet', datamap.setVars, source || "datamapGet");
- if (typeof datamap.setVars.prop === 'string' && datamap.setVars.prop.indexOf('.') > -1) {
- var sliced = datamap.setVars.prop.split(".");
- var out = priv.settings.data[datamap.setVars.row];
- for (var i = 0, ilen = sliced.length - 1; i < ilen; i++) {
- out = out[sliced[i]];
- }
- out[sliced[i]] = datamap.setVars.value;
- }
- else if (typeof datamap.setVars.prop === 'function') {
- /* see the `function` handler in `get` */
- datamap.setVars.prop(priv.settings.data.slice(
- datamap.setVars.row,
- datamap.setVars.row + 1
- )[0], datamap.setVars.value);
- }
- else {
- priv.settings.data[datamap.setVars.row][datamap.setVars.prop] = datamap.setVars.value;
- }
- },
- /**
- * Clears the data array
- */
- clear: function () {
- for (var r = 0; r < instance.countRows(); r++) {
- for (var c = 0; c < instance.countCols(); c++) {
- datamap.set(r, datamap.colToProp(c), '');
- }
- }
- },
- /**
- * Returns the data array
- * @return {Array}
- */
- getAll: function () {
- return priv.settings.data;
- },
- /**
- * Returns data range as array
- * @param {Object} start Start selection position
- * @param {Object} end End selection position
- * @return {Array}
- */
- getRange: function (start, end) {
- var r, rlen, c, clen, output = [], row;
- rlen = Math.max(start.row, end.row);
- clen = Math.max(start.col, end.col);
- for (r = Math.min(start.row, end.row); r <= rlen; r++) {
- row = [];
- for (c = Math.min(start.col, end.col); c <= clen; c++) {
- row.push(datamap.get(r, datamap.colToProp(c)));
- }
- output.push(row);
- }
- return output;
- },
- /**
- * Return data as text (tab separated columns)
- * @param {Object} start (Optional) Start selection position
- * @param {Object} end (Optional) End selection position
- * @return {String}
- */
- getText: function (start, end) {
- return SheetClip.stringify(datamap.getRange(start, end));
- }
- };
- grid = {
- /**
- * Inserts or removes rows and columns
- * @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 (action, index, amount, source, keepEmptyRows) {
- var oldData, newData, changes, r, rlen, c, clen, delta;
- oldData = $.extend(true, [], datamap.getAll());
- switch (action) {
- case "insert_row":
- if (!amount) {
- amount = 1;
- }
- delta = 0;
- while (delta < amount && instance.countRows() < priv.settings.maxRows) {
- datamap.createRow(index);
- delta++;
- }
- if (delta) {
- if (priv.selStart.exists() && priv.selStart.row() >= index) {
- priv.selStart.row(priv.selStart.row() + delta);
- selection.transformEnd(delta, 0); //will call render() internally
- }
- else {
- selection.refreshBorders(); //it will call render and prepare methods
- }
- }
- break;
- case "insert_col":
- if (!amount) {
- amount = 1;
- }
- delta = 0;
- while (delta < amount && instance.countCols() < priv.settings.maxCols) {
- datamap.createCol(index);
- delta++;
- }
- if (delta) {
- if (priv.selStart.exists() && priv.selStart.col() >= index) {
- priv.selStart.col(priv.selStart.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);
- grid.adjustRowsAndCols();
- selection.refreshBorders(); //it will call render and prepare methods
- break;
- case "remove_col":
- datamap.removeCol(index, amount);
- grid.adjustRowsAndCols();
- selection.refreshBorders(); //it will call render and prepare methods
- break;
- default:
- throw new Error('There is no such action "' + action + '"');
- break;
- }
- changes = [];
- newData = datamap.getAll();
- for (r = 0, rlen = newData.length; r < rlen; r++) {
- for (c = 0, clen = newData[r].length; c < clen; c++) {
- changes.push([r, c, oldData[r] ? oldData[r][c] : null, newData[r][c]]);
- }
- }
- instance.PluginHooks.run('afterChange', changes, source || 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 () {
- var r, rlen, emptyRows = instance.countEmptyRows(true), emptyCols;
- //should I add empty rows to data source to meet minRows?
- rlen = instance.countRows();
- if (rlen < priv.settings.minRows) {
- for (r = 0; r < priv.settings.minRows - rlen; r++) {
- datamap.createRow();
- }
- }
- //should I add empty rows to meet minSpareRows?
- if (emptyRows < priv.settings.minSpareRows) {
- for (; emptyRows < priv.settings.minSpareRows && instance.countRows() < priv.settings.maxRows; emptyRows++) {
- datamap.createRow();
- }
- }
- //count currently empty cols
- emptyCols = instance.countEmptyCols(true);
- //should I add empty cols to meet minCols?
- if (!priv.settings.columns && instance.countCols() < priv.settings.minCols) {
- for (; instance.countCols() < priv.settings.minCols; emptyCols++) {
- datamap.createCol();
- }
- }
- //should I add empty cols to meet minSpareCols?
- if (!priv.settings.columns && priv.dataType === 'array' && emptyCols < priv.settings.minSpareCols) {
- for (; emptyCols < priv.settings.minSpareCols && instance.countCols() < priv.settings.maxCols; emptyCols++) {
- datamap.createCol();
- }
- }
- if (priv.settings.enterBeginsEditing) {
- for (; (((priv.settings.minRows || priv.settings.minSpareRows) && instance.countRows() > priv.settings.minRows) && (priv.settings.minSpareRows && emptyRows > priv.settings.minSpareRows)); emptyRows--) {
- datamap.removeRow();
- }
- }
- if (priv.settings.enterBeginsEditing && !priv.settings.columns) {
- for (; (((priv.settings.minCols || priv.settings.minSpareCols) && instance.countCols() > priv.settings.minCols) && (priv.settings.minSpareCols && emptyCols > priv.settings.minSpareCols)); emptyCols--) {
- datamap.removeCol();
- }
- }
- var rowCount = instance.countRows();
- var colCount = instance.countCols();
- if (rowCount === 0 || colCount === 0) {
- selection.deselect();
- }
- if (priv.selStart.exists()) {
- var selectionChanged;
- var fromRow = priv.selStart.row();
- var fromCol = priv.selStart.col();
- var toRow = priv.selEnd.row();
- var toCol = priv.selEnd.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);
- }
- }
- },
- /**
- * Populate cells at position with 2d array
- * @param {Object} start Start selection position
- * @param {Array} input 2d array
- * @param {Object} [end] End selection position (only for drag-down mode)
- * @param {String} [source="populateFromArray"]
- * @param {String} [method="overwrite"]
- * @return {Object|undefined} ending td in pasted area (only if any cell was changed)
- */
- populateFromArray: function (start, input, end, source, method) {
- var r, rlen, c, clen, setData = [], current = {};
- rlen = input.length;
- if (rlen === 0) {
- return false;
- }
- var repeatCol
- , repeatRow
- , cmax
- , rmax;
- // 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 = Handsontable.helper.translateRowsToColumns(input);
- for (c = 0, clen = input.length, cmax = Math.max(clen, repeatCol); c < cmax; c++) {
- if (c < clen) {
- 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.spliceCol.apply(instance, input[c]);
- }
- else {
- input[c % clen][0] = start.col + c;
- instance.spliceCol.apply(instance, 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) {
- 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);
- instance.spliceRow.apply(instance, input[r]);
- }
- else {
- input[r % rlen][0] = start.row + r;
- instance.spliceRow.apply(instance, input[r % rlen]);
- }
- }
- break;
- case 'overwrite' :
- default:
- // overwrite and other not specified options
- current.row = start.row;
- current.col = start.col;
- for (r = 0; r < rlen; r++) {
- if ((end && current.row > end.row) || (!priv.settings.minSpareRows && current.row > instance.countRows() - 1) || (current.row >= priv.settings.maxRows)) {
- break;
- }
- current.col = start.col;
- clen = input[r] ? input[r].length : 0;
- for (c = 0; c < clen; c++) {
- if ((end && current.col > end.col) || (!priv.settings.minSpareCols && current.col > instance.countCols() - 1) || (current.col >= priv.settings.maxCols)) {
- break;
- }
- if (instance.getCellMeta(current.row, current.col).isWritable) {
- setData.push([current.row, current.col, input[r][c]]);
- }
- current.col++;
- if (end && c === clen - 1) {
- c = -1;
- }
- }
- current.row++;
- if (end && r === rlen - 1) {
- r = -1;
- }
- }
- instance.setDataAtCell(setData, null, null, source || 'populateFromArray');
- break;
- }
- },
- /**
- * Returns the top left (TL) and bottom right (BR) selection coordinates
- * @param {Object[]} coordsArr
- * @returns {Object}
- */
- getCornerCoords: function (coordsArr) {
- function mapProp(func, array, prop) {
- function getProp(el) {
- return el[prop];
- }
- if (Array.prototype.map) {
- return func.apply(Math, array.map(getProp));
- }
- return func.apply(Math, $.map(array, getProp));
- }
- return {
- TL: {
- row: mapProp(Math.min, coordsArr, "row"),
- col: mapProp(Math.min, coordsArr, "col")
- },
- BR: {
- row: mapProp(Math.max, coordsArr, "row"),
- col: mapProp(Math.max, coordsArr, "col")
- }
- };
- },
- /**
- * Returns array of td objects given start and end coordinates
- */
- getCellsAtCoords: function (start, end) {
- var corners = grid.getCornerCoords([start, end]);
- var r, c, output = [];
- for (r = corners.TL.row; r <= corners.BR.row; r++) {
- for (c = corners.TL.col; c <= corners.BR.col; c++) {
- output.push(instance.view.getCellAtCoords({
- row: r,
- col: c
- }));
- }
- }
- return output;
- }
- };
- this.selection = selection = { //this public assignment is only temporary
- inProgress: false,
- /**
- * Sets inProgress to true. This enables onSelectionEnd and onSelectionEndByProp to function as desired
- */
- begin: function () {
- instance.selection.inProgress = true;
- },
- /**
- * Sets inProgress to false. Triggers onSelectionEnd and onSelectionEndByProp
- */
- finish: function () {
- var sel = instance.getSelected();
- instance.PluginHooks.run("afterSelectionEnd", sel[0], sel[1], sel[2], sel[3]);
- instance.PluginHooks.run("afterSelectionEndByProp", sel[0], instance.colToProp(sel[1]), sel[2], instance.colToProp(sel[3]));
- instance.selection.inProgress = false;
- },
- isInProgress: function () {
- return instance.selection.inProgress;
- },
- /**
- * Starts selection range on given td object
- * @param {Object} coords
- */
- setRangeStart: function (coords) {
- priv.selStart.coords(coords);
- selection.setRangeEnd(coords);
- },
- /**
- * Ends selection range on given td object
- * @param {Object} coords
- * @param {Boolean} [scrollToCell=true] If true, viewport will be scrolled to range end
- */
- setRangeEnd: function (coords, scrollToCell) {
- instance.selection.begin();
- priv.selEnd.coords(coords);
- if (!priv.settings.multiSelect) {
- priv.selStart.coords(coords);
- }
- //set up current selection
- instance.view.wt.selections.current.clear();
- instance.view.wt.selections.current.add(priv.selStart.arr());
- //set up area selection
- instance.view.wt.selections.area.clear();
- if (selection.isMultiple()) {
- instance.view.wt.selections.area.add(priv.selStart.arr());
- instance.view.wt.selections.area.add(priv.selEnd.arr());
- }
- //set up highlight
- if (priv.settings.currentRowClassName || priv.settings.currentColClassName) {
- instance.view.wt.selections.highlight.clear();
- instance.view.wt.selections.highlight.add(priv.selStart.arr());
- instance.view.wt.selections.highlight.add(priv.selEnd.arr());
- }
- //trigger handlers
- instance.PluginHooks.run("afterSelection", priv.selStart.row(), priv.selStart.col(), priv.selEnd.row(), priv.selEnd.col());
- instance.PluginHooks.run("afterSelectionByProp", priv.selStart.row(), datamap.colToProp(priv.selStart.col()), priv.selEnd.row(), datamap.colToProp(priv.selEnd.col()));
- if (scrollToCell !== false) {
- instance.view.scrollViewport(coords);
- instance.view.wt.draw(true); //these two lines are needed to fix scrolling viewport when cell dimensions are significantly bigger than assumed by Walkontable
- instance.view.scrollViewport(coords);
- }
- selection.refreshBorders();
- },
- /**
- * Destroys editor, redraws borders around cells, prepares editor
- * @param {Boolean} revertOriginal
- * @param {Boolean} keepEditor
- */
- refreshBorders: function (revertOriginal, keepEditor) {
- if (!keepEditor) {
- editproxy.destroy(revertOriginal);
- }
- instance.view.render();
- if (selection.isSelected() && !keepEditor) {
- editproxy.prepare();
- }
- },
- /**
- * Returns information if we have a multiselection
- * @return {Boolean}
- */
- isMultiple: function () {
- return !(priv.selEnd.col() === priv.selStart.col() && priv.selEnd.row() === priv.selStart.row());
- },
- /**
- * Selects cell relative to current cell (if possible)
- */
- transformStart: function (rowDelta, colDelta, force) {
- if (priv.selStart.row() + rowDelta > instance.countRows() - 1) {
- if (force && priv.settings.minSpareRows > 0) {
- instance.alter("insert_row", instance.countRows());
- }
- else if (priv.settings.autoWrapCol && priv.selStart.col() + colDelta < instance.countCols() - 1) {
- rowDelta = 1 - instance.countRows();
- colDelta = 1;
- }
- }
- else if (priv.settings.autoWrapCol && priv.selStart.row() + rowDelta < 0 && priv.selStart.col() + colDelta >= 0) {
- rowDelta = instance.countRows() - 1;
- colDelta = -1;
- }
- if (priv.selStart.col() + colDelta > instance.countCols() - 1) {
- if (force && priv.settings.minSpareCols > 0) {
- instance.alter("insert_col", instance.countCols());
- }
- else if (priv.settings.autoWrapRow && priv.selStart.row() + rowDelta < instance.countRows() - 1) {
- rowDelta = 1;
- colDelta = 1 - instance.countCols();
- }
- }
- else if (priv.settings.autoWrapRow && priv.selStart.col() + colDelta < 0 && priv.selStart.row() + rowDelta >= 0) {
- rowDelta = -1;
- colDelta = instance.countCols() - 1;
- }
- var totalRows = instance.countRows();
- var totalCols = instance.countCols();
- var coords = {
- row: (priv.selStart.row() + rowDelta),
- col: priv.selStart.col() + colDelta
- };
- if (coords.row < 0) {
- coords.row = 0;
- }
- else if (coords.row > 0 && coords.row >= totalRows) {
- coords.row = totalRows - 1;
- }
- if (coords.col < 0) {
- coords.col = 0;
- }
- else if (coords.col > 0 && coords.col >= totalCols) {
- coords.col = totalCols - 1;
- }
- selection.setRangeStart(coords);
- },
- /**
- * Sets selection end cell relative to current selection end cell (if possible)
- */
- transformEnd: function (rowDelta, colDelta) {
- if (priv.selEnd.exists()) {
- var totalRows = instance.countRows();
- var totalCols = instance.countCols();
- var coords = {
- row: priv.selEnd.row() + rowDelta,
- col: priv.selEnd.col() + colDelta
- };
- if (coords.row < 0) {
- coords.row = 0;
- }
- else if (coords.row > 0 && coords.row >= totalRows) {
- coords.row = totalRows - 1;
- }
- if (coords.col < 0) {
- coords.col = 0;
- }
- else if (coords.col > 0 && coords.col >= totalCols) {
- coords.col = totalCols - 1;
- }
- selection.setRangeEnd(coords);
- }
- },
- /**
- * Returns true if currently there is a selection on screen, false otherwise
- * @return {Boolean}
- */
- isSelected: function () {
- return priv.selEnd.exists();
- },
- /**
- * Returns true if coords is within current selection coords
- * @return {Boolean}
- */
- inInSelection: function (coords) {
- if (!selection.isSelected()) {
- return false;
- }
- var sel = grid.getCornerCoords([priv.selStart.coords(), priv.selEnd.coords()]);
- return (sel.TL.row <= coords.row && sel.BR.row >= coords.row && sel.TL.col <= coords.col && sel.BR.col >= coords.col);
- },
- /**
- * Deselects all selected cells
- */
- deselect: function () {
- if (!selection.isSelected()) {
- return;
- }
- instance.selection.inProgress = false; //needed by HT inception
- priv.selEnd = new Handsontable.SelectionPoint(); //create new empty point to remove the existing one
- instance.view.wt.selections.current.clear();
- instance.view.wt.selections.area.clear();
- editproxy.destroy();
- selection.refreshBorders();
- instance.PluginHooks.run('afterDeselect');
- },
- /**
- * Select all cells
- */
- selectAll: function () {
- if (!priv.settings.multiSelect) {
- return;
- }
- selection.setRangeStart({
- row: 0,
- col: 0
- });
- selection.setRangeEnd({
- row: instance.countRows() - 1,
- col: instance.countCols() - 1
- }, false);
- },
- /**
- * Deletes data from selected cells
- */
- empty: function () {
- if (!selection.isSelected()) {
- return;
- }
- var corners = grid.getCornerCoords([priv.selStart.coords(), priv.selEnd.coords()]);
- var r, c, changes = [];
- for (r = corners.TL.row; r <= corners.BR.row; r++) {
- for (c = corners.TL.col; c <= corners.BR.col; c++) {
- if (instance.getCellMeta(r, c).isWritable) {
- changes.push([r, c, '']);
- }
- }
- }
- instance.setDataAtCell(changes);
- }
- };
- this.autofill = autofill = { //this public assignment is only temporary
- handle: null,
- /**
- * Create fill handle and fill border objects
- */
- init: function () {
- if (!autofill.handle) {
- autofill.handle = {};
- }
- else {
- autofill.handle.disabled = false;
- }
- },
- /**
- * Hide fill handle and fill border permanently
- */
- disable: function () {
- autofill.handle.disabled = true;
- },
- /**
- * Selects cells down to the last row in the left column, then fills down to that cell
- */
- selectAdjacent: function () {
- var select, data, r, maxR, c;
- if (selection.isMultiple()) {
- select = instance.view.wt.selections.area.getCorners();
- }
- else {
- select = instance.view.wt.selections.current.getCorners();
- }
- data = datamap.getAll();
- rows : for (r = select[2] + 1; r < instance.countRows(); r++) {
- for (c = select[1]; c <= select[3]; c++) {
- if (data[r][c]) {
- break rows;
- }
- }
- if (!!data[r][select[1] - 1] || !!data[r][select[3] + 1]) {
- maxR = r;
- }
- }
- if (maxR) {
- instance.view.wt.selections.fill.clear();
- instance.view.wt.selections.fill.add([select[0], select[1]]);
- instance.view.wt.selections.fill.add([maxR, select[3]]);
- autofill.apply();
- }
- },
- /**
- * Apply fill values to the area in fill border, omitting the selection border
- */
- apply: function () {
- var drag, select, start, end, _data;
- autofill.handle.isDragged = 0;
- drag = instance.view.wt.selections.fill.getCorners();
- if (!drag) {
- return;
- }
- instance.view.wt.selections.fill.clear();
- if (selection.isMultiple()) {
- select = instance.view.wt.selections.area.getCorners();
- }
- else {
- select = instance.view.wt.selections.current.getCorners();
- }
- if (drag[0] === select[0] && drag[1] < select[1]) {
- start = {
- row: drag[0],
- col: drag[1]
- };
- end = {
- row: drag[2],
- col: select[1] - 1
- };
- }
- else if (drag[0] === select[0] && drag[3] > select[3]) {
- start = {
- row: drag[0],
- col: select[3] + 1
- };
- end = {
- row: drag[2],
- col: drag[3]
- };
- }
- else if (drag[0] < select[0] && drag[1] === select[1]) {
- start = {
- row: drag[0],
- col: drag[1]
- };
- end = {
- row: select[0] - 1,
- col: drag[3]
- };
- }
- else if (drag[2] > select[2] && drag[1] === select[1]) {
- start = {
- row: select[2] + 1,
- col: drag[1]
- };
- end = {
- row: drag[2],
- col: drag[3]
- };
- }
- if (start) {
- _data = SheetClip.parse(datamap.getText(priv.selStart.coords(), priv.selEnd.coords()));
- instance.PluginHooks.run('beforeAutofill', start, end, _data);
- grid.populateFromArray(start, _data, end, 'autofill');
- selection.setRangeStart({row: drag[0], col: drag[1]});
- selection.setRangeEnd({row: drag[2], col: drag[3]});
- }
- /*else {
- //reset to avoid some range bug
- selection.refreshBorders();
- }*/
- },
- /**
- * Show fill border
- */
- showBorder: function (coords) {
- coords.row = coords[0];
- coords.col = coords[1];
- var corners = grid.getCornerCoords([priv.selStart.coords(), priv.selEnd.coords()]);
- if (priv.settings.fillHandle !== 'horizontal' && (corners.BR.row < coords.row || corners.TL.row > coords.row)) {
- coords = [coords.row, corners.BR.col];
- }
- else if (priv.settings.fillHandle !== 'vertical') {
- coords = [corners.BR.row, coords.col];
- }
- else {
- return; //wrong direction
- }
- instance.view.wt.selections.fill.clear();
- instance.view.wt.selections.fill.add([priv.selStart.coords().row, priv.selStart.coords().col]);
- instance.view.wt.selections.fill.add([priv.selEnd.coords().row, priv.selEnd.coords().col]);
- instance.view.wt.selections.fill.add(coords);
- instance.view.render();
- }
- };
- editproxy = { //this public assignment is only temporary
- /**
- * Create input field
- */
- init: function () {
- function onCut() {
- selection.empty();
- }
- function onPaste(str) {
- var input = str.replace(/^[\r\n]*/g, '').replace(/[\r\n]*$/g, '') //remove newline from the start and the end of the input
- , inputArray = SheetClip.parse(input)
- , coords = grid.getCornerCoords([priv.selStart.coords(), priv.selEnd.coords()])
- , areaStart = coords.TL
- , areaEnd = {
- row: Math.max(coords.BR.row, inputArray.length - 1 + coords.TL.row),
- col: Math.max(coords.BR.col, inputArray[0].length - 1 + coords.TL.col)
- };
- instance.PluginHooks.once('afterChange', function (changes, source) {
- if (changes && changes.length) {
- instance.selectCell(areaStart.row, areaStart.col, areaEnd.row, areaEnd.col);
- }
- });
- grid.populateFromArray(areaStart, inputArray, areaEnd, 'paste', priv.settings.pasteMode);
- }
- var $body = $(document.body);
- function onKeyDown(event) {
- if (priv.settings.beforeOnKeyDown) { // HOT in HOT Plugin
- priv.settings.beforeOnKeyDown.call(instance, event);
- }
- if ($body.children('.context-menu-list:visible').length) {
- return;
- }
- if (event.keyCode === 17 || event.keyCode === 224 || event.keyCode === 91 || event.keyCode === 93) {
- //when CTRL is pressed, prepare selectable text in textarea
- //http://stackoverflow.com/questions/3902635/how-does-one-capture-a-macs-command-key-via-javascript
- editproxy.setCopyableText();
- return;
- }
- priv.lastKeyCode = event.keyCode;
- if (selection.isSelected()) {
- var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; //catch CTRL but not right ALT (which in some systems triggers ALT+CTRL)
- if (Handsontable.helper.isPrintableChar(event.keyCode) && ctrlDown) {
- if (event.keyCode === 65) { //CTRL + A
- selection.selectAll(); //select all cells
- editproxy.setCopyableText();
- event.preventDefault();
- }
- else if (event.keyCode === 89 || (event.shiftKey && event.keyCode === 90)) { //CTRL + Y or CTRL + SHIFT + Z
- priv.undoRedo && priv.undoRedo.redo();
- }
- else if (event.keyCode === 90) { //CTRL + Z
- priv.undoRedo && priv.undoRedo.undo();
- }
- return;
- }
- var rangeModifier = event.shiftKey ? selection.setRangeEnd : selection.setRangeStart;
- instance.PluginHooks.run('beforeKeyDown', event);
- if (!event.isImmediatePropagationStopped()) {
- switch (event.keyCode) {
- case 38: /* arrow up */
- if (event.shiftKey) {
- selection.transformEnd(-1, 0);
- }
- else {
- selection.transformStart(-1, 0);
- }
- event.preventDefault();
- event.stopPropagation(); //required by HandsontableEditor
- break;
- case 9: /* tab */
- var tabMoves = typeof priv.settings.tabMoves === 'function' ? priv.settings.tabMoves(event) : priv.settings.tabMoves;
- if (event.shiftKey) {
- selection.transformStart(-tabMoves.row, -tabMoves.col); //move selection left
- }
- else {
- selection.transformStart(tabMoves.row, tabMoves.col, true); //move selection right (add a new column if needed)
- }
- event.preventDefault();
- event.stopPropagation(); //required by HandsontableEditor
- break;
- case 39: /* arrow right */
- if (event.shiftKey) {
- selection.transformEnd(0, 1);
- }
- else {
- selection.transformStart(0, 1);
- }
- event.preventDefault();
- event.stopPropagation(); //required by HandsontableEditor
- break;
- case 37: /* arrow left */
- if (event.shiftKey) {
- selection.transformEnd(0, -1);
- }
- else {
- selection.transformStart(0, -1);
- }
- event.preventDefault();
- event.stopPropagation(); //required by HandsontableEditor
- break;
- case 8: /* backspace */
- case 46: /* delete */
- selection.empty(event);
- event.preventDefault();
- break;
- case 40: /* arrow down */
- if (event.shiftKey) {
- selection.transformEnd(1, 0); //expanding selection down with shift
- }
- else {
- selection.transformStart(1, 0); //move selection down
- }
- event.preventDefault();
- event.stopPropagation(); //required by HandsontableEditor
- break;
- case 113: /* F2 */
- event.preventDefault(); //prevent Opera from opening Go to Page dialog
- break;
- case 13: /* return/enter */
- var enterMoves = typeof priv.settings.enterMoves === 'function' ? priv.settings.enterMoves(event) : priv.settings.enterMoves;
- if (event.shiftKey) {
- selection.transformStart(-enterMoves.row, -enterMoves.col); //move selection up
- }
- else {
- selection.transformStart(enterMoves.row, enterMoves.col, true); //move selection down (add a new row if needed)
- }
- event.preventDefault(); //don't add newline to field
- break;
- case 36: /* home */
- if (event.ctrlKey || event.metaKey) {
- rangeModifier({row: 0, col: priv.selStart.col()});
- }
- else {
- rangeModifier({row: priv.selStart.row(), col: 0});
- }
- event.preventDefault(); //don't scroll the window
- event.stopPropagation(); //required by HandsontableEditor
- break;
- case 35: /* end */
- if (event.ctrlKey || event.metaKey) {
- rangeModifier({row: instance.countRows() - 1, col: priv.selStart.col()});
- }
- else {
- rangeModifier({row: priv.selStart.row(), col: instance.countCols() - 1});
- }
- event.preventDefault(); //don't scroll the window
- event.stopPropagation(); //required by HandsontableEditor
- break;
- case 33: /* pg up */
- selection.transformStart(-instance.countVisibleRows(), 0);
- instance.view.wt.scrollVertical(-instance.countVisibleRows());
- instance.view.render();
- event.preventDefault(); //don't page up the window
- event.stopPropagation(); //required by HandsontableEditor
- break;
- case 34: /* pg down */
- selection.transformStart(instance.countVisibleRows(), 0);
- instance.view.wt.scrollVertical(instance.countVisibleRows());
- instance.view.render();
- event.preventDefault(); //don't page down the window
- event.stopPropagation(); //required by HandsontableEditor
- break;
- default:
- break;
- }
- }
- }
- }
- instance.copyPaste = new CopyPaste(instance.rootElement[0]);
- instance.copyPaste.onCut(onCut);
- instance.copyPaste.onPaste(onPaste);
- instance.rootElement.on('keydown.handsontable.' + instance.guid, onKeyDown);
- },
- /**
- * Destroy current editor, if exists
- * @param {Boolean} revertOriginal
- */
- destroy: function (revertOriginal) {
- if (typeof priv.editorDestroyer === "function") {
- var destroyer = priv.editorDestroyer; //this copy is needed, otherwise destroyer can enter an infinite loop
- priv.editorDestroyer = null;
- destroyer(revertOriginal);
- }
- },
- /**
- * Prepares copyable text in the invisible textarea
- */
- setCopyableText: function () {
- var startRow = Math.min(priv.selStart.row(), priv.selEnd.row());
- var startCol = Math.min(priv.selStart.col(), priv.selEnd.col());
- var endRow = Math.max(priv.selStart.row(), priv.selEnd.row());
- var endCol = Math.max(priv.selStart.col(), priv.selEnd.col());
- var finalEndRow = Math.min(endRow, startRow + priv.settings.copyRowsLimit - 1);
- var finalEndCol = Math.min(endCol, startCol + priv.settings.copyColsLimit - 1);
- instance.copyPaste.copyable(datamap.getText({row: startRow, col: startCol}, {row: finalEndRow, col: finalEndCol}));
- if (endRow !== finalEndRow || endCol !== finalEndCol) {
- instance.PluginHooks.run("afterCopyLimit", endRow - startRow + 1, endCol - startCol + 1, priv.settings.copyRowsLimit, priv.settings.copyColsLimit);
- }
- },
- /**
- * Prepare text input to be displayed at given grid cell
- */
- prepare: function () {
- if (!instance.getCellMeta(priv.selStart.row(), priv.selStart.col()).isWritable) {
- return;
- }
- instance.listen();
- var TD = instance.view.getCellAtCoords(priv.selStart.coords());
- priv.editorDestroyer = instance.view.applyCellTypeMethod('editor', TD, priv.selStart.row(), priv.selStart.col());
- //presumably TD can be removed from here. Cell editor should also listen for changes if editable cell is outside from viewport
- }
- };
- this.init = function () {
- instance.PluginHooks.run('beforeInit');
- editproxy.init();
- this.updateSettings(priv.settings, true);
- this.parseSettingsFromDOM();
- this.focusCatcher = new Handsontable.FocusCatcher(this);
- this.view = new Handsontable.TableView(this);
- this.forceFullRender = true; //used when data was changed
- this.view.render();
- if (typeof priv.firstRun === 'object') {
- instance.PluginHooks.run('afterChange', priv.firstRun[0], priv.firstRun[1]);
- priv.firstRun = false;
- }
- instance.PluginHooks.run('afterInit');
- };
- function validateChanges(changes, source) {
- var validated = $.Deferred();
- var deferreds = [];
- //validate strict autocompletes
- var process = function (i) {
- var deferred = $.Deferred();
- deferreds.push(deferred);
- var originalVal = changes[i][3];
- var lowercaseVal = typeof originalVal === 'string' ? originalVal.toLowerCase() : null;
- return function (source) {
- var found = false;
- for (var s = 0, slen = source.length; s < slen; s++) {
- if (originalVal === source[s]) {
- found = true; //perfect match
- break;
- }
- else if (lowercaseVal === source[s].toLowerCase()) {
- changes[i][3]