/** * Handsontable 0.8.8 * 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: Mon Mar 04 2013 00:45:03 GMT+0100 (Central European Standard 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 settings * @constructor */ Handsontable.Core = function (rootElement, settings) { this.rootElement = rootElement; var priv, datamap, grid, selection, editproxy, autofill, validate, self = this; priv = { settings: {}, 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.PluginModifiers.run(self, 'col', 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.PluginModifiers.run(self, 'col', 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 {Object} [coords] Optional. Coords of the cell before which the new row will be inserted */ createRow: function (coords) { var row; if (priv.dataType === 'array') { row = []; for (var c = 0, clen = self.countCols(); c < clen; c++) { row.push(null); } } else { row = $.extend(true, {}, datamap.getSchema()); } if (!coords || coords.row >= self.countRows()) { if (priv.settings.onCreateRow) { priv.settings.onCreateRow(self.countRows(), row); } priv.settings.data.push(row); } else { if (priv.settings.onCreateRow) { priv.settings.onCreateRow(coords.row, row); } priv.settings.data.splice(coords.row, 0, row); } self.forceFullRender = true; //used when data was changed }, /** * Creates col at the right of the data array * @param {Object} [coords] Optional. Coords of the cell before which the new column will be inserted */ createCol: function (coords) { 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 = self.countRows(); if (!coords || coords.col >= self.countCols()) { for (; r < rlen; r++) { if (typeof priv.settings.data[r] === 'undefined') { priv.settings.data[r] = []; } priv.settings.data[r].push(''); } } else { for (; r < rlen; r++) { priv.settings.data[r].splice(coords.col, 0, ''); } } self.forceFullRender = true; //used when data was changed }, /** * Removes row at the bottom of the data array * @param {Object} [coords] Optional. Coords of the cell which row will be removed * @param {Object} [toCoords] Required if coords is defined. Coords of the cell until which all rows will be removed */ removeRow: function (coords, toCoords) { if (!coords || coords.row === self.countRows() - 1) { priv.settings.data.pop(); } else { priv.settings.data.splice(coords.row, toCoords.row - coords.row + 1); } self.forceFullRender = true; //used when data was changed }, /** * Removes col at the right of the data array * @param {Object} [coords] Optional. Coords of the cell which col will be removed * @param {Object} [toCoords] Required if coords is defined. Coords of the cell until which all cols will be removed */ removeCol: function (coords, toCoords) { if (priv.dataType === 'object' || priv.settings.columns) { throw new Error("cannot remove column with object data source or columns option specified"); } var r = 0; if (!coords || coords.col === self.countCols() - 1) { for (; r < self.countRows(); r++) { priv.settings.data[r].pop(); } } else { var howMany = toCoords.col - coords.col + 1; for (; r < self.countRows(); r++) { priv.settings.data[r].splice(coords.col, howMany); } } self.forceFullRender = true; //used when data was changed }, /** * 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; Handsontable.PluginHooks.run(self, '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 { 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 */ setVars: {}, set: function (row, prop, value) { datamap.setVars.row = row; datamap.setVars.prop = prop; datamap.setVars.value = value; Handsontable.PluginHooks.run(self, 'beforeSet', datamap.setVars); 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 { priv.settings.data[datamap.setVars.row][datamap.setVars.prop] = datamap.setVars.value; } }, /** * Clears the data array */ clear: function () { for (var r = 0; r < self.countRows(); r++) { for (var c = 0; c < self.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 = { /** * Alter grid * @param {String} action Possible values: "insert_row", "insert_col", "remove_row", "remove_col" * @param {Object} coords * @param {Object} [toCoords] Required only for actions "remove_row" and "remove_col" */ alter: function (action, coords, toCoords) { var oldData, newData, changes, r, rlen, c, clen; oldData = $.extend(true, [], datamap.getAll()); switch (action) { case "insert_row": if (self.countRows() < priv.settings.maxRows) { datamap.createRow(coords); if (priv.selStart.exists() && priv.selStart.row() >= coords.row) { priv.selStart.row(priv.selStart.row() + 1); selection.transformEnd(1, 0); //will call render() internally } else { selection.refreshBorders(); //it will call render and prepare methods } } break; case "insert_col": if (self.countCols() < priv.settings.maxCols) { datamap.createCol(coords); if (priv.selStart.exists() && priv.selStart.col() >= coords.col) { priv.selStart.col(priv.selStart.col() + 1); selection.transformEnd(0, 1); //will call render() internally } else { selection.refreshBorders(); //it will call render and prepare methods } } break; case "remove_row": datamap.removeRow(coords, toCoords); grid.keepEmptyRows(); selection.refreshBorders(); //it will call render and prepare methods break; case "remove_col": datamap.removeCol(coords, toCoords); grid.keepEmptyRows(); selection.refreshBorders(); //it will call render and prepare methods 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]]); } } fireEvent("datachange.handsontable", [changes, 'alter']); grid.keepEmptyRows(); //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 */ keepEmptyRows: function () { var r, c, rlen, clen, emptyRows = 0, emptyCols = 0, val; //count currently empty rows rows : for (r = self.countRows() - 1; r >= 0; r--) { for (c = 0, clen = self.countCols(); c < clen; c++) { val = datamap.get(r, datamap.colToProp(c)); if (val !== '' && val !== null && typeof val !== 'undefined') { break rows; } } emptyRows++; } //should I add empty rows to data source to meet startRows? rlen = self.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 && self.countRows() < priv.settings.maxRows; emptyRows++) { datamap.createRow(); } } //count currently empty cols if (self.countRows() - 1 > 0) { cols : for (c = self.countCols() - 1; c >= 0; c--) { for (r = 0; r < self.countRows(); r++) { val = datamap.get(r, datamap.colToProp(c)); if (val !== '' && val !== null && typeof val !== 'undefined') { break cols; } } emptyCols++; } } //should I add empty cols to meet minCols? if (!priv.settings.columns && self.countCols() < priv.settings.minCols) { for (; self.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 && self.countCols() < priv.settings.maxCols; emptyCols++) { datamap.createCol(); } } if (priv.settings.enterBeginsEditing) { for (; (((priv.settings.minRows || priv.settings.minSpareRows) && self.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) && self.countCols() > priv.settings.minCols) && (priv.settings.minSpareCols && emptyCols > priv.settings.minSpareCols)); emptyCols--) { datamap.removeCol(); } } var rowCount = self.countRows(); var colCount = self.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) { self.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"] * @return {Object|undefined} ending td in pasted area (only if any cell was changed) */ populateFromArray: function (start, input, end, source) { var r, rlen, c, clen, setData = [], current = {}; rlen = input.length; if (rlen === 0) { return false; } 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 > self.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 > self.countCols() - 1) || (current.col >= priv.settings.maxCols)) { break; } if (self.getCellMeta(current.row, current.col).isWritable) { var p = datamap.colToProp(current.col); setData.push([current.row, p, input[r][c]]); } current.col++; if (end && c === clen - 1) { c = -1; } } current.row++; if (end && r === rlen - 1) { r = -1; } } self.setDataAtCell(setData, null, null, source || 'populateFromArray'); }, /** * 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(self.view.getCellAtCoords({ row: r, col: c })); } } return output; } }; this.selection = selection = { //this public assignment is only temporary /** * Starts selection range on given td object * @param {Object} coords */ setRangeStart: function (coords) { selection.deselect(); 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) { priv.selEnd.coords(coords); if (!priv.settings.multiSelect) { priv.selStart.coords(coords); } //set up current selection self.view.wt.selections.current.clear(); self.view.wt.selections.current.add(priv.selStart.arr()); //set up area selection self.view.wt.selections.area.clear(); if (selection.isMultiple()) { self.view.wt.selections.area.add(priv.selStart.arr()); self.view.wt.selections.area.add(priv.selEnd.arr()); } //trigger handlers self.rootElement.triggerHandler("selection.handsontable", [priv.selStart.row(), priv.selStart.col(), priv.selEnd.row(), priv.selEnd.col()]); self.rootElement.triggerHandler("selectionbyprop.handsontable", [priv.selStart.row(), datamap.colToProp(priv.selStart.col()), priv.selEnd.row(), datamap.colToProp(priv.selEnd.col())]); if (scrollToCell !== false) { self.view.scrollViewport(coords); self.view.wt.draw(true); //these two lines are needed to fix scrolling viewport when cell dimensions are significantly bigger than assumed by Walkontable self.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); } self.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 > self.countRows() - 1) { if (force && priv.settings.minSpareRows > 0) { self.alter("insert_row", self.countRows()); } else if (priv.settings.autoWrapCol && priv.selStart.col() + colDelta < self.countCols() - 1) { rowDelta = 1 - self.countRows(); colDelta = 1; } } else if (priv.settings.autoWrapCol && priv.selStart.row() + rowDelta < 0 && priv.selStart.col() + colDelta >= 0) { rowDelta = self.countRows() - 1; colDelta = -1; } if (priv.selStart.col() + colDelta > self.countCols() - 1) { if (force && priv.settings.minSpareCols > 0) { self.alter("insert_col", self.countCols()); } else if (priv.settings.autoWrapRow && priv.selStart.row() + rowDelta < self.countRows() - 1) { rowDelta = 1; colDelta = 1 - self.countCols(); } } else if (priv.settings.autoWrapRow && priv.selStart.col() + colDelta < 0 && priv.selStart.row() + rowDelta >= 0) { rowDelta = -1; colDelta = self.countCols() - 1; } var totalRows = self.countRows(); var totalCols = self.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 = self.countRows(); var totalCols = self.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; } priv.selEnd = new Handsontable.SelectionPoint(); //create new empty point to remove the existing one self.view.wt.selections.current.clear(); self.view.wt.selections.area.clear(); editproxy.destroy(); selection.refreshBorders(); self.rootElement.triggerHandler('deselect.handsontable'); }, /** * Select all cells */ selectAll: function () { if (!priv.settings.multiSelect) { return; } selection.setRangeStart({ row: 0, col: 0 }); selection.setRangeEnd({ row: self.countRows() - 1, col: self.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 (self.getCellMeta(r, c).isWritable) { changes.push([r, datamap.colToProp(c), '']); } } } self.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 = self.view.wt.selections.area.getCorners(); } else { select = self.view.wt.selections.current.getCorners(); } data = datamap.getAll(); rows : for (r = select[2] + 1; r < self.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) { self.view.wt.selections.fill.clear(); self.view.wt.selections.fill.add([select[0], select[1]]); self.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; autofill.handle.isDragged = 0; drag = self.view.wt.selections.fill.getCorners(); if (!drag) { return; } self.view.wt.selections.fill.clear(); if (selection.isMultiple()) { select = self.view.wt.selections.area.getCorners(); } else { select = self.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) { grid.populateFromArray(start, SheetClip.parse(datamap.getText(priv.selStart.coords(), priv.selEnd.coords())), 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 } self.view.wt.selections.fill.clear(); self.view.wt.selections.fill.add([priv.selStart.coords().row, priv.selStart.coords().col]); self.view.wt.selections.fill.add([priv.selEnd.coords().row, priv.selEnd.coords().col]); self.view.wt.selections.fill.add(coords); self.view.render(); } }; editproxy = { //this public assignment is only temporary /** * Create input field */ init: function () { function onCut() { selection.empty(); } function onPaste(str) { self.rootElement.one("datachange.handsontable", function (event, changes, source) { if (changes.length) { var last = changes[changes.length - 1]; selection.setRangeEnd({row: last[0], col: self.propToCol(last[1])}); } }); 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()]); grid.populateFromArray(coords.TL, inputArray, { 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) }, 'paste'); } var $body = $(document.body); function onKeyDown(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; switch (event.keyCode) { case 38: /* arrow up */ if (event.shiftKey) { selection.transformEnd(-1, 0); } else { selection.transformStart(-1, 0); } event.preventDefault(); 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(); break; case 39: /* arrow right */ if (event.shiftKey) { selection.transformEnd(0, 1); } else { selection.transformStart(0, 1); } event.preventDefault(); break; case 37: /* arrow left */ if (event.shiftKey) { selection.transformEnd(0, -1); } else { selection.transformStart(0, -1); } event.preventDefault(); 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(); 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}); } break; case 35: /* end */ if (event.ctrlKey || event.metaKey) { rangeModifier({row: self.countRows() - 1, col: priv.selStart.col()}); } else { rangeModifier({row: priv.selStart.row(), col: self.countCols() - 1}); } break; case 33: /* pg up */ selection.transformStart(-self.countVisibleRows(), 0); self.view.wt.scrollVertical(-self.countVisibleRows()); self.view.render(); event.preventDefault(); //don't page up the window break; case 34: /* pg down */ selection.transformStart(self.countVisibleRows(), 0); self.view.wt.scrollVertical(self.countVisibleRows()); self.view.render(); event.preventDefault(); //don't page down the window break; default: break; } } } self.copyPaste = new CopyPaste(self.rootElement[0]); self.copyPaste.onCut(onCut); self.copyPaste.onPaste(onPaste); self.rootElement.on('keydown.handsontable', onKeyDown); }, /** * Destroy current editor, if exists * @param {Boolean} revertOriginal */ destroy: function (revertOriginal) { if (typeof priv.editorDestroyer === "function") { priv.editorDestroyer(revertOriginal); priv.editorDestroyer = null; } }, /** * 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); self.copyPaste.copyable(datamap.getText({row: startRow, col: startCol}, {row: finalEndRow, col: finalEndCol})); if ((endRow !== finalEndRow || endCol !== finalEndCol) && priv.settings.onCopyLimit) { priv.settings.onCopyLimit(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 (priv.settings.asyncRendering) { clearTimeout(window.prepareFrame); window.prepareFrame = setTimeout(function () { var TD = self.view.getCellAtCoords(priv.selStart.coords()); TD.focus(); priv.editorDestroyer = self.view.applyCellTypeMethod('editor', TD, priv.selStart.row(), priv.selStart.col()); }, 0); } else { var TD = self.view.getCellAtCoords(priv.selStart.coords()); TD.focus(); priv.editorDestroyer = self.view.applyCellTypeMethod('editor', TD, priv.selStart.row(), priv.selStart.col()); } } }; this.init = function () { Handsontable.PluginHooks.run(self, 'beforeInit'); editproxy.init(); bindEvents(); this.updateSettings(settings); this.view = new Handsontable.TableView(this); if (typeof priv.firstRun === 'object') { fireEvent('datachange.handsontable', priv.firstRun); priv.firstRun = false; } Handsontable.PluginHooks.run(self, 'afterInit'); }; validate = function (changes, source) { var validated = $.Deferred(); var deferreds = []; if (source === 'paste') { //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] = source[s]; //good match, fix the case found = true; break; } } if (!found) { changes[i] = null; } deferred.resolve(); } }; for (var i = changes.length - 1; i >= 0; i--) { var cellProperties = self.getCellMeta(changes[i][0], datamap.propToCol(changes[i][1])); if (cellProperties.strict && cellProperties.source) { var items = $.isFunction(cellProperties.source) ? cellProperties.source(changes[i][3], process(i)) : cellProperties.source; if (items) { process(i)(items) } } } } $.when(deferreds).then(function () { for (var i = changes.length - 1; i >= 0; i--) { if (changes[i] === null) { changes.splice(i, 1); } var cellProperties = self.getCellMeta(changes[i][0], datamap.propToCol(changes[i][1])); if (cellProperties.dataType === 'number' && typeof changes[i][3] === 'string') { if (changes[i][3].length > 0 && /^[0-9\s]*[.]*[0-9]*$/.test(changes[i][3])) { changes[i][3] = numeral().unformat(changes[i][3] || '0'); //numeral cannot unformat empty string } } } if (priv.settings.onBeforeChange && changes.length) { var result = priv.settings.onBeforeChange.apply(self.rootElement[0], [changes, source]); if (typeof result === 'function') { $.when(result).then(function () { validated.resolve(); }); } else { if (result === false) { changes.splice(0, changes.length); //invalidate all changes (remove everything from array) } validated.resolve(); } } else { validated.resolve(); } }); return $.when(validated); }; var fireEvent = function (name, params) { if (priv.settings.asyncRendering) { setTimeout(function () { self.rootElement.triggerHandler(name, params); }, 0); } else { self.rootElement.triggerHandler(name, params); } }; var bindEvents = function () { self.rootElement.on("datachange.handsontable", function (event, changes, source) { if (priv.settings.onChange) { priv.settings.onChange.apply(self.rootElement[0], [changes, source]); } }); self.rootElement.on("selection.handsontable", function (event, row, col, endRow, endCol) { if (priv.settings.onSelection) { priv.settings.onSelection.apply(self.rootElement[0], [row, col, endRow, endCol]); } }); self.rootElement.on("selectionbyprop.handsontable", function (event, row, prop, endRow, endProp) { if (priv.settings.onSelectionByProp) { priv.settings.onSelectionByProp.apply(self.rootElement[0], [row, prop, endRow, endProp]); } }); }; /** * Set data at given cell * @public * @param {Number|Array} row or array of changes in format [[row, col, value], ...] * @param {Number} prop * @param {String} value * @param {String} [source='edit'] String that identifies how this change will be described in changes array (useful in onChange callback) */ this.setDataAtCell = function (row, prop, value, source) { var changes, i, ilen; if (typeof row === "object") { //is it an array of changes changes = row; } else if ($.isPlainObject(value)) { //backwards compatibility changes = value; } else { changes = [ [row, prop, value] ]; } for (i = 0, ilen = changes.length; i < ilen; i++) { changes[i].splice(2, 0, datamap.get(changes[i][0], changes[i][1])); //add old value at index 2 } validate(changes, source).then(function () { //when validate is resolved... for (i = 0, ilen = changes.length; i < ilen; i++) { row = changes[i][0]; prop = changes[i][1]; var col = datamap.propToCol(prop); value = changes[i][3]; if (priv.settings.minSpareRows) { while (row > self.countRows() - 1) { datamap.createRow(); } } if (priv.dataType === 'array' && priv.settings.minSpareCols) { while (col > self.countCols() - 1) { datamap.createCol(); } } datamap.set(row, prop, value); } self.forceFullRender = true; //used when data was changed grid.keepEmptyRows(); selection.refreshBorders(); fireEvent("datachange.handsontable", [changes, source || 'edit']); }); }; /** * Destroys current editor, renders and selects current cell. If revertOriginal != true, edited data is saved * @param {Boolean} revertOriginal */ this.destroyEditor = function (revertOriginal) { selection.refreshBorders(revertOriginal); }; /** * 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"] * @return {Object|undefined} ending td in pasted area (only if any cell was changed) */ this.populateFromArray = function (start, input, end, source) { return grid.populateFromArray(start, input, end, source); }; /** * Returns the top left (TL) and bottom right (BR) selection coordinates * @param {Object[]} coordsArr * @returns {Object} */ this.getCornerCoords = function (coordsArr) { return grid.getCornerCoords(coordsArr); }; /** * Returns current selection. Returns undefined if there is no selection. * @public * @return {Array} [topLeftRow, topLeftCol, bottomRightRow, bottomRightCol] */ this.getSelected = function () { //https://github.com/warpech/jquery-handsontable/issues/44 //cjl if (selection.isSelected()) { var coords = grid.getCornerCoords([priv.selStart.coords(), priv.selEnd.coords()]); return [coords.TL.row, coords.TL.col, coords.BR.row, coords.BR.col]; } }; /** * Render visible data * @public */ this.render = function () { if (self.view) { self.forceFullRender = true; //used when data was changed selection.refreshBorders(null, true); } }; /** * Load data from array * @public * @param {Array} data */ this.loadData = function (data) { priv.isPopulated = false; priv.settings.data = data; if ($.isPlainObject(priv.settings.dataSchema) || $.isPlainObject(data[0])) { priv.dataType = 'object'; } else { priv.dataType = 'array'; } if (data[0]) { priv.duckDataSchema = datamap.recursiveDuckSchema(data[0]); } else { priv.duckDataSchema = {}; } datamap.createMap(); grid.keepEmptyRows(); Handsontable.PluginHooks.run(self, 'afterLoadData'); if (priv.firstRun) { priv.firstRun = [null, 'loadData']; } else { fireEvent('datachange.handsontable', [null, 'loadData']); self.render(); } priv.isPopulated = true; self.clearUndo(); }; /** * Return the current data object (the same that was passed by `data` configuration option or `loadData` method). Optionally you can provide cell range `r`, `c`, `r2`, `c2` to get only a fragment of grid data * @public * @param {Number} r (Optional) From row * @param {Number} c (Optional) From col * @param {Number} r2 (Optional) To row * @param {Number} c2 (Optional) To col * @return {Array|Object} */ this.getData = function (r, c, r2, c2) { if (typeof r === 'undefined') { return datamap.getAll(); } else { return datamap.getRange({row: r, col: c}, {row: r2, col: c2}); } }; /** * Update settings * @public */ this.updateSettings = function (settings) { var i; if (typeof settings.rows !== "undefined") { throw new Error("'rows' setting is no longer supported. do you mean startRows, minRows or maxRows?"); } if (typeof settings.cols !== "undefined") { throw new Error("'cols' setting is no longer supported. do you mean startCols, minCols or maxCols?"); } if (typeof settings.undo !== "undefined") { if (priv.undoRedo && settings.undo === false) { priv.undoRedo = null; } else if (!priv.undoRedo && settings.undo === true) { priv.undoRedo = new Handsontable.UndoRedo(self); } } for (i in settings) { if (i === 'data') { continue; //loadData will be triggered later } else if (settings.hasOwnProperty(i)) { priv.settings[i] = settings[i]; //launch extensions if (Handsontable.extension[i]) { priv.extensions[i] = new Handsontable.extension[i](self, settings[i]); } } } if (settings.data === void 0 && priv.settings.data === void 0) { var data = []; var row; for (var r = 0, rlen = priv.settings.startRows; r < rlen; r++) { row = []; for (var c = 0, clen = priv.settings.startCols; c < clen; c++) { row.push(null); } data.push(row); } self.loadData(data); //data source created just now } else if (settings.data !== void 0) { self.loadData(settings.data); //data source given as option } else if (settings.columns !== void 0) { datamap.createMap(); } if (typeof settings.fillHandle !== "undefined") { if (autofill.handle && settings.fillHandle === false) { autofill.disable(); } else if (!autofill.handle && settings.fillHandle !== false) { autofill.init(); } } grid.keepEmptyRows(); if (self.view) { self.forceFullRender = true; //used when data was changed selection.refreshBorders(null, true); } }; /** * Returns current settings object * @return {Object} */ this.getSettings = function () { return priv.settings; }; /** * Clears grid * @public */ this.clear = function () { selection.selectAll(); selection.empty(); }; /** * Return true if undo can be performed, false otherwise * @public */ this.isUndoAvailable = function () { return priv.undoRedo && priv.undoRedo.isUndoAvailable(); }; /** * Return true if redo can be performed, false otherwise * @public */ this.isRedoAvailable = function () { return priv.undoRedo && priv.undoRedo.isRedoAvailable(); }; /** * Undo last edit * @public */ this.undo = function () { priv.undoRedo && priv.undoRedo.undo(); }; /** * Redo edit (used to reverse an undo) * @public */ this.redo = function () { priv.undoRedo && priv.undoRedo.redo(); }; /** * Clears undo history * @public */ this.clearUndo = function () { priv.undoRedo && priv.undoRedo.clear(); }; /** * Alters the grid * @param {String} action See grid.alter for possible values * @param {Number} from * @param {Number} [to] Optional. Used only for actions "remove_row" and "remove_col" * @public */ this.alter = function (action, from, to) { if (typeof to === "undefined") { to = from; } switch (action) { case "insert_row": case "remove_row": grid.alter(action, {row: from, col: 0}, {row: to, col: 0}); break; case "insert_col": case "remove_col": grid.alter(action, {row: 0, col: from}, {row: 0, col: to}); break; default: throw Error('There is no such action "' + action + '"'); break; } }; /** * Returns