From 6c831eb17068ce5e193e78f06ce70b6ba00b8aa9 Mon Sep 17 00:00:00 2001 From: Marcin Warpechowski Date: Tue, 26 Mar 2013 02:40:14 +0100 Subject: [PATCH] feature: split-screen.html demo now uses all available space in window --- CHANGELOG.md | 8 + dist/angular-ui-handsontable.full.css | 41 +- dist/angular-ui-handsontable.full.js | 1955 +++++++++++------ dist/angular-ui-handsontable.full.min.css | 4 +- dist/angular-ui-handsontable.full.min.js | 12 +- js/split-screen.js | 29 + split-screen.html | 2 +- .../handsontable/jquery.handsontable.full.css | 39 +- .../handsontable/jquery.handsontable.full.js | 1953 ++++++++++------ 9 files changed, 2674 insertions(+), 1369 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89765c28..fabd12e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## HEAD + +Feature: +- split-screen.html demo now uses all available space in window + +Other: +- upgrade Handsontable to 0.8.16 + ## 0.3.5 (Mar 24, 2012) Bugfix: diff --git a/dist/angular-ui-handsontable.full.css b/dist/angular-ui-handsontable.full.css index 11d34d68..f8540360 100644 --- a/dist/angular-ui-handsontable.full.css +++ b/dist/angular-ui-handsontable.full.css @@ -1,18 +1,18 @@ /** * angular-ui-handsontable 0.3.5 * - * Date: Sun Mar 24 2013 17:47:35 GMT+0100 (Central European Standard Time) + * Date: Tue Mar 26 2013 02:37:48 GMT+0100 (Central European Standard Time) */ /** - * Handsontable 0.8.8 + * Handsontable 0.8.16 * 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) + * Date: Tue Mar 26 2013 02:34:10 GMT+0100 (Central European Standard Time) */ .handsontable { @@ -22,6 +22,13 @@ font-size: 13px; } +.handsontable.hidden { + display : none; + left : 0; + position :absolute; + top : 0; +} + .handsontable * { box-sizing: content-box; -webkit-box-sizing: content-box; @@ -43,6 +50,10 @@ table-layout: fixed; width: 0; outline-width: 0; + /* reset bootstrap table style. for more info see: https://github.com/warpech/jquery-handsontable/issues/224 */ + max-width : none; + max-height: none; +} } .handsontable col { @@ -198,6 +209,15 @@ textarea.handsontableInput { font-size: 13px; box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); resize: none; + + /*below are needed to overwrite stuff added by jQuery UI Bootstrap theme*/ + display: inline-block; + font-size: 13px; + line-height: inherit; + color: #000; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; } .handsontableInputHolder { @@ -239,7 +259,7 @@ NumericRenderer } /* typeahead rules. Needed only if you are using the autocomplete feature */ -.typeahead { +.handsontable .typeahead { position: absolute; font-family: Arial, Helvetica, sans-serif; line-height: 1.3em; @@ -270,28 +290,33 @@ NumericRenderer border-radius: 4px; } -.typeahead li { +.handsontable .typeahead li { line-height: 18px; + min-height: 18px; display: list-item; + margin: 0; } -.typeahead a { +.handsontable .typeahead a { display: block; padding: 3px 15px; clear: both; font-weight: normal; line-height: 18px; + min-height: 18px; color: #333; white-space: nowrap; } -.typeahead li > a:hover, .typeahead .active > a, .typeahead .active > a:hover { +.handsontable .typeahead li > a:hover, +.handsontable .typeahead .active > a, +.handsontable .typeahead .active > a:hover { color: white; text-decoration: none; background-color: #08C; } -.typeahead a { +.handsontable .typeahead a { color: #08C; text-decoration: none; } diff --git a/dist/angular-ui-handsontable.full.js b/dist/angular-ui-handsontable.full.js index 34c2f297..7554282d 100644 --- a/dist/angular-ui-handsontable.full.js +++ b/dist/angular-ui-handsontable.full.js @@ -1,7 +1,7 @@ /** * angular-ui-handsontable 0.3.5 * - * Date: Sun Mar 24 2013 17:47:35 GMT+0100 (Central European Standard Time) + * Date: Tue Mar 26 2013 02:37:48 GMT+0100 (Central European Standard Time) */ /** @@ -400,14 +400,14 @@ angular.module('uiHandsontable', []) return directiveDefinitionObject; }); /** - * Handsontable 0.8.8 + * Handsontable 0.8.16 * 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) + * Date: Tue Mar 26 2013 02:34:10 GMT+0100 (Central European Standard Time) */ /*jslint white: true, browser: true, plusplus: true, indent: 4, maxerr: 50 */ @@ -426,8 +426,13 @@ var Handsontable = { //class namespace */ Handsontable.Core = function (rootElement, settings) { this.rootElement = rootElement; + this.guid = 'ht_' + Handsontable.helper.randomString(); //this is the namespace for global events - var priv, datamap, grid, selection, editproxy, autofill, validate, self = this; + if (!this.rootElement[0].id) { + this.rootElement[0].id = this.guid; //if root element does not have an id, assign a random id + } + + var priv, datamap, grid, selection, editproxy, autofill, self = this; priv = { settings: {}, @@ -543,44 +548,50 @@ Handsontable.Core = function (rootElement, settings) { /** * 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 + * @param {Number} [index] Optional. Index of the row before which the new row will be inserted */ - createRow: function (coords) { - var row; + createRow: function (index) { + var row + , rowCount = self.countRows(); + + if (typeof index !== 'number' || index >= rowCount) { + index = rowCount; + } + if (priv.dataType === 'array') { row = []; for (var c = 0, clen = self.countCols(); c < clen; c++) { row.push(null); } } + else if (priv.dataType === 'function') { + row = priv.settings.dataSchema(index); + } else { row = $.extend(true, {}, datamap.getSchema()); } - if (!coords || coords.row >= self.countRows()) { - if (priv.settings.onCreateRow) { - priv.settings.onCreateRow(self.countRows(), row); - } + if (priv.settings.onCreateRow) { + priv.settings.onCreateRow(index, row); + } + if (index === rowCount) { priv.settings.data.push(row); } else { - if (priv.settings.onCreateRow) { - priv.settings.onCreateRow(coords.row, row); - } - priv.settings.data.splice(coords.row, 0, row); + priv.settings.data.splice(index, 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 + * @param {Object} [index] Optional. Index of the column before which the new column will be inserted */ - createCol: function (coords) { + 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 = self.countRows(); - if (!coords || coords.col >= self.countCols()) { + if (typeof index !== 'number' || index >= self.countCols()) { for (; r < rlen; r++) { if (typeof priv.settings.data[r] === 'undefined') { priv.settings.data[r] = []; @@ -590,47 +601,45 @@ Handsontable.Core = function (rootElement, settings) { } else { for (; r < rlen; r++) { - priv.settings.data[r].splice(coords.col, 0, ''); + priv.settings.data[r].splice(index, 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 + * 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 (coords, toCoords) { - if (!coords || coords.row === self.countRows() - 1) { - priv.settings.data.pop(); + removeRow: function (index, amount) { + if (!amount) { + amount = 1; } - else { - priv.settings.data.splice(coords.row, toCoords.row - coords.row + 1); + if (typeof index !== 'number') { + index = -amount; } + priv.settings.data.splice(index, amount); 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 + * 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 (coords, toCoords) { + 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"); } - var r = 0; - if (!coords || coords.col === self.countCols() - 1) { - for (; r < self.countRows(); r++) { - priv.settings.data[r].pop(); - } + if (!amount) { + amount = 1; } - else { - var howMany = toCoords.col - coords.col + 1; - for (; r < self.countRows(); r++) { - priv.settings.data[r].splice(coords.col, howMany); - } + if (typeof index !== 'number') { + index = -amount; + } + for (var r = 0, rlen = self.countRows(); r < rlen; r++) { + priv.settings.data[r].splice(index, amount); } self.forceFullRender = true; //used when data was changed }, @@ -659,6 +668,25 @@ Handsontable.Core = function (rootElement, settings) { } 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; } @@ -684,6 +712,13 @@ Handsontable.Core = function (rootElement, settings) { } 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; } @@ -741,22 +776,29 @@ Handsontable.Core = function (rootElement, settings) { grid = { /** - * Alter grid + * Inserts or removes rows and columns * @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" + * @param {Number} index + * @param {Number} amount */ - alter: function (action, coords, toCoords) { - var oldData, newData, changes, r, rlen, c, clen; + alter: function (action, index, amount) { + var oldData, newData, changes, r, rlen, c, clen, delta; 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 + if (!amount) { + amount = 1; + } + delta = 0; + while (delta < amount && self.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 @@ -765,11 +807,18 @@ Handsontable.Core = function (rootElement, settings) { 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 + if (!amount) { + amount = 1; + } + delta = 0; + while (delta < amount && self.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 @@ -778,16 +827,20 @@ Handsontable.Core = function (rootElement, settings) { break; case "remove_row": - datamap.removeRow(coords, toCoords); + datamap.removeRow(index, amount); grid.keepEmptyRows(); selection.refreshBorders(); //it will call render and prepare methods break; case "remove_col": - datamap.removeCol(coords, toCoords); + datamap.removeCol(index, amount); grid.keepEmptyRows(); selection.refreshBorders(); //it will call render and prepare methods break; + + default: + throw Error('There is no such action "' + action + '"'); + break; } changes = []; @@ -805,20 +858,9 @@ Handsontable.Core = function (rootElement, settings) { * 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++; - } + var r, rlen, emptyRows = self.countEmptyRows(true), emptyCols; - //should I add empty rows to data source to meet startRows? + //should I add empty rows to data source to meet minRows? rlen = self.countRows(); if (rlen < priv.settings.minRows) { for (r = 0; r < priv.settings.minRows - rlen; r++) { @@ -834,17 +876,7 @@ Handsontable.Core = function (rootElement, settings) { } //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++; - } - } + emptyCols = self.countEmptyCols(true); //should I add empty cols to meet minCols? if (!priv.settings.columns && self.countCols() < priv.settings.minCols) { @@ -949,8 +981,7 @@ Handsontable.Core = function (rootElement, settings) { break; } if (self.getCellMeta(current.row, current.col).isWritable) { - var p = datamap.colToProp(current.col); - setData.push([current.row, p, input[r][c]]); + setData.push([current.row, current.col, input[r][c]]); } current.col++; if (end && c === clen - 1) { @@ -1013,12 +1044,34 @@ Handsontable.Core = function (rootElement, settings) { }; 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 () { + self.selection.inProgress = true; + }, + + /** + * Sets inProgress to false. Triggers onSelectionEnd and onSelectionEndByProp + */ + finish: function () { + var sel = self.getSelected(); + self.rootElement.triggerHandler("selectionend.handsontable", sel); + self.rootElement.triggerHandler("selectionendbyprop.handsontable", [sel[0], self.colToProp(sel[1]), sel[2], self.colToProp(sel[3])]); + self.selection.inProgress = false; + }, + + isInProgress: function () { + return self.selection.inProgress; + }, + /** * Starts selection range on given td object * @param {Object} coords */ setRangeStart: function (coords) { - selection.deselect(); priv.selStart.coords(coords); selection.setRangeEnd(coords); }, @@ -1029,6 +1082,8 @@ Handsontable.Core = function (rootElement, settings) { * @param {Boolean} [scrollToCell=true] If true, viewport will be scrolled to range end */ setRangeEnd: function (coords, scrollToCell) { + self.selection.begin(); + priv.selEnd.coords(coords); if (!priv.settings.multiSelect) { priv.selStart.coords(coords); @@ -1192,6 +1247,7 @@ Handsontable.Core = function (rootElement, settings) { if (!selection.isSelected()) { return; } + self.selection.inProgress = false; //needed by HT inception 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(); @@ -1229,7 +1285,7 @@ Handsontable.Core = function (rootElement, settings) { 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), '']); + changes.push([r, c, '']); } } } @@ -1422,6 +1478,10 @@ Handsontable.Core = function (rootElement, settings) { var $body = $(document.body); function onKeyDown(event) { + if (priv.settings.beforeOnKeyDown) { + priv.settings.beforeOnKeyDown.call(self, event); + } + if ($body.children('.context-menu-list:visible').length) { return; } @@ -1462,6 +1522,7 @@ Handsontable.Core = function (rootElement, settings) { selection.transformStart(-1, 0); } event.preventDefault(); + event.stopPropagation(); //required by HandsontableEditor break; case 9: /* tab */ @@ -1473,6 +1534,7 @@ Handsontable.Core = function (rootElement, settings) { 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 */ @@ -1483,6 +1545,7 @@ Handsontable.Core = function (rootElement, settings) { selection.transformStart(0, 1); } event.preventDefault(); + event.stopPropagation(); //required by HandsontableEditor break; case 37: /* arrow left */ @@ -1493,6 +1556,7 @@ Handsontable.Core = function (rootElement, settings) { selection.transformStart(0, -1); } event.preventDefault(); + event.stopPropagation(); //required by HandsontableEditor break; case 8: /* backspace */ @@ -1509,6 +1573,7 @@ Handsontable.Core = function (rootElement, settings) { selection.transformStart(1, 0); //move selection down } event.preventDefault(); + event.stopPropagation(); //required by HandsontableEditor break; case 113: /* F2 */ @@ -1533,6 +1598,7 @@ Handsontable.Core = function (rootElement, settings) { else { rangeModifier({row: priv.selStart.row(), col: 0}); } + event.stopPropagation(); //required by HandsontableEditor break; case 35: /* end */ @@ -1542,6 +1608,7 @@ Handsontable.Core = function (rootElement, settings) { else { rangeModifier({row: priv.selStart.row(), col: self.countCols() - 1}); } + event.stopPropagation(); //required by HandsontableEditor break; case 33: /* pg up */ @@ -1549,6 +1616,7 @@ Handsontable.Core = function (rootElement, settings) { self.view.wt.scrollVertical(-self.countVisibleRows()); self.view.render(); event.preventDefault(); //don't page up the window + event.stopPropagation(); //required by HandsontableEditor break; case 34: /* pg down */ @@ -1556,6 +1624,7 @@ Handsontable.Core = function (rootElement, settings) { self.view.wt.scrollVertical(self.countVisibleRows()); self.view.render(); event.preventDefault(); //don't page down the window + event.stopPropagation(); //required by HandsontableEditor break; default: @@ -1567,7 +1636,7 @@ Handsontable.Core = function (rootElement, settings) { self.copyPaste = new CopyPaste(self.rootElement[0]); self.copyPaste.onCut(onCut); self.copyPaste.onPaste(onPaste); - self.rootElement.on('keydown.handsontable', onKeyDown); + self.rootElement.on('keydown.handsontable.' + self.guid, onKeyDown); }, /** @@ -1576,8 +1645,9 @@ Handsontable.Core = function (rootElement, settings) { */ destroy: function (revertOriginal) { if (typeof priv.editorDestroyer === "function") { - priv.editorDestroyer(revertOriginal); + var destroyer = priv.editorDestroyer; //this copy is needed, otherwise destroyer can enter an infinite loop priv.editorDestroyer = null; + destroyer(revertOriginal); } }, @@ -1603,17 +1673,26 @@ Handsontable.Core = function (rootElement, settings) { * Prepare text input to be displayed at given grid cell */ prepare: function () { + if (!self.getCellMeta(priv.selStart.row(), priv.selStart.col()).isWritable) { + return; + } + if (priv.settings.asyncRendering) { - clearTimeout(window.prepareFrame); - window.prepareFrame = setTimeout(function () { + self.registerTimeout('prepareFrame', function () { var TD = self.view.getCellAtCoords(priv.selStart.coords()); - TD.focus(); + if (Handsontable.helper.isDescendant(self.rootElement[0], document.activeElement)) { + //we don't want to steal focus if it is outside HT (issue #408) + 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(); + if (Handsontable.helper.isDescendant(self.rootElement[0], document.activeElement)) { + //we don't want to steal focus if it is outside HT (issue #408) + TD.focus(); + } priv.editorDestroyer = self.view.applyCellTypeMethod('editor', TD, priv.selStart.row(), priv.selStart.col()); } } @@ -1634,60 +1713,56 @@ Handsontable.Core = function (rootElement, settings) { Handsontable.PluginHooks.run(self, 'afterInit'); }; - validate = function (changes, source) { + function validateChanges(changes, source) { var validated = $.Deferred(); var deferreds = []; - if (source === 'paste') { - //validate strict autocompletes - var process = function (i) { - var deferred = $.Deferred(); - deferreds.push(deferred); + //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; + 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; - } + 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; } - if (!found) { - changes[i] = null; + else if (lowercaseVal === source[s].toLowerCase()) { + changes[i][3] = source[s]; //good match, fix the case + found = true; + break; } - 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) - } + 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) { + $.isFunction(cellProperties.source) ? cellProperties.source(changes[i][3], process(i)) : process(i)(cellProperties.source); } } - $.when(deferreds).then(function () { + $.when.apply($, deferreds).then(function () { for (var i = changes.length - 1; i >= 0; i--) { if (changes[i] === null) { changes.splice(i, 1); - } + } else { + var cellProperties = self.getCellMeta(changes[i][0], datamap.propToCol(changes[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 (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 + } } } } @@ -1712,11 +1787,11 @@ Handsontable.Core = function (rootElement, settings) { }); return $.when(validated); - }; + } var fireEvent = function (name, params) { if (priv.settings.asyncRendering) { - setTimeout(function () { + self.registerTimeout('fireEvent', function () { self.rootElement.triggerHandler(name, params); }, 0); } @@ -1741,58 +1816,124 @@ Handsontable.Core = function (rootElement, settings) { priv.settings.onSelectionByProp.apply(self.rootElement[0], [row, prop, endRow, endProp]); } }); + self.rootElement.on("selectionend.handsontable", function (event, row, col, endRow, endCol) { + if (priv.settings.onSelectionEnd) { + priv.settings.onSelectionEnd.apply(self.rootElement[0], [row, col, endRow, endCol]); + } + }); + self.rootElement.on("selectionendbyprop.handsontable", function (event, row, prop, endRow, endProp) { + if (priv.settings.onSelectionEndByProp) { + priv.settings.onSelectionEndByProp.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) + * Internal function to apply changes. Called after validateChanges + * @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) */ - this.setDataAtCell = function (row, prop, value, source) { - var changes, i, ilen; + function applyChanges(changes, source) { + var i = 0 + , ilen = changes.length; + + if (!ilen) { + return; + } + + while (i < ilen) { + if (priv.settings.minSpareRows) { + while (changes[i][0] > self.countRows() - 1) { + datamap.createRow(); + } + } + if (priv.dataType === 'array' && priv.settings.minSpareCols) { + while (datamap.propToCol(changes[i][1]) > self.countCols() - 1) { + datamap.createCol(); + } + } + datamap.set(changes[i][0], changes[i][1], changes[i][3]); + i++; + } + self.forceFullRender = true; //used when data was changed + grid.keepEmptyRows(); + selection.refreshBorders(); + fireEvent("datachange.handsontable", [changes, source || 'edit']); + } - if (typeof row === "object") { //is it an array of changes - changes = row; + function setDataInputToArray(arg0, arg1, arg2) { + if (typeof arg0 === "object") { //is it an array of changes + return arg0; } - else if ($.isPlainObject(value)) { //backwards compatibility - changes = value; + else if ($.isPlainObject(arg2)) { //backwards compatibility + return value; } else { - changes = [ - [row, prop, value] + return [ + [arg0, arg1, arg2] ]; } + } + + /** + * Set data at given cell + * @public + * @param {Number|Array} row or array of changes in format [[row, col, value], ...] + * @param {Number} col + * @param {String} value + * @param {String} source String that identifies how this change will be described in changes array (useful in onChange callback) + */ + this.setDataAtCell = function (row, col, value, source) { + var input = setDataInputToArray(row, col, value) + , i + , ilen + , changes = [] + , prop; - 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 + for (i = 0, ilen = input.length; i < ilen; i++) { + 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, + datamap.get(input[i][0], prop), + input[i][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]; + validateChanges(changes, source).then(function () { + applyChanges(changes, source); + }); + }; - 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']); + + /** + * Set data at given row property + * @public + * @param {Number|Array} row or array of changes in format [[row, prop, value], ...] + * @param {Number} prop + * @param {String} value + * @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], + datamap.get(input[i][0], input[i][1]), + input[i][2] + ]); + } + + validateChanges(changes, source).then(function () { + applyChanges(changes, source); }); }; @@ -1828,12 +1969,11 @@ Handsontable.Core = function (rootElement, settings) { /** * Returns current selection. Returns undefined if there is no selection. * @public - * @return {Array} [topLeftRow, topLeftCol, bottomRightRow, bottomRightCol] + * @return {Array} [`startRow`, `startCol`, `endRow`, `endCol`] */ 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]; + return [priv.selStart.row(), priv.selStart.col(), priv.selEnd.row(), priv.selEnd.col()]; } }; @@ -1854,13 +1994,20 @@ Handsontable.Core = function (rootElement, settings) { * @param {Array} data */ this.loadData = function (data) { + if (!(data instanceof Array)) { + throw new Error("loadData only accepts array of objects or array of arrays (" + typeof data + " given)"); + } + priv.isPopulated = false; priv.settings.data = data; - if ($.isPlainObject(priv.settings.dataSchema) || $.isPlainObject(data[0])) { - priv.dataType = 'object'; + if (priv.settings.dataSchema instanceof Array || data[0] instanceof Array) { + priv.dataType = 'array'; + } + else if ($.isFunction(priv.settings.dataSchema)) { + priv.dataType = 'function'; } else { - priv.dataType = 'array'; + priv.dataType = 'object'; } if (data[0]) { priv.duckDataSchema = datamap.recursiveDuckSchema(data[0]); @@ -2032,31 +2179,14 @@ Handsontable.Core = function (rootElement, settings) { }; /** - * Alters the grid + * Inserts or removes rows and columns * @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" + * @param {Number} index + * @param {Number} amount * @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; - } + this.alter = function (action, index, amount) { + grid.alter(action, index, amount); }; /** @@ -2091,16 +2221,27 @@ Handsontable.Core = function (rootElement, settings) { }; /** - * Return cell value at `row`, `col` + * Return value at `row`, `col` * @param {Number} row * @param {Number} col * @public - * @return {string} + * @return value (mixed data type) */ this.getDataAtCell = function (row, col) { return datamap.get(row, datamap.colToProp(col)); }; + /** + * Return value at `row`, `prop` + * @param {Number} row + * @param {Number} prop + * @public + * @return value (mixed data type) + */ + this.getDataAtRowProp = function (row, prop) { + return datamap.get(row, prop); + }; + /** * Returns cell meta data object corresponding to params row, col * @param {Number} row @@ -2187,7 +2328,7 @@ Handsontable.Core = function (rootElement, settings) { while (dividend > 0) { modulo = (dividend - 1) % 26; columnLabel = String.fromCharCode(65 + modulo) + columnLabel; - dividend = parseInt((dividend - modulo) / 26); + dividend = parseInt((dividend - modulo) / 26, 10); } DIV.innerHTML = '' + columnLabel + ''; } @@ -2236,7 +2377,7 @@ Handsontable.Core = function (rootElement, settings) { * @return {Number} */ this.countCols = function () { - if (priv.dataType === 'object') { + if (priv.dataType === 'object' || priv.dataType === 'function') { if (priv.settings.columns && priv.settings.columns.length) { return priv.settings.columns.length; } @@ -2248,8 +2389,11 @@ Handsontable.Core = function (rootElement, settings) { if (priv.settings.columns && priv.settings.columns.length) { return priv.settings.columns.length; } + else if (priv.settings.data && priv.settings.data[0] && priv.settings.data[0].length) { + return priv.settings.data[0].length; + } else { - return Math.max((priv.settings.columns && priv.settings.columns.length) || 0, (priv.settings.data && priv.settings.data[0] && priv.settings.data[0].length) || 0); + return 0; } } }; @@ -2286,6 +2430,88 @@ Handsontable.Core = function (rootElement, settings) { return self.view.wt.getSetting('viewportColumns'); }; + /** + * Return number of empty rows + * @return {Boolean} ending If true, will only count empty rows at the end of the data source + */ + this.countEmptyRows = function (ending) { + var i = self.countRows() - 1 + , empty = 0; + while (i >= 0) { + if (self.isEmptyRow(i)) { + empty++; + } + else if (ending) { + break; + } + i--; + } + return empty; + }; + + /** + * Return number of empty columns + * @return {Boolean} ending If true, will only count empty columns at the end of the data source row + */ + this.countEmptyCols = function (ending) { + if (self.countRows() < 1) { + return 0; + } + + var i = self.countCols() - 1 + , empty = 0; + while (i >= 0) { + if (self.isEmptyCol(i)) { + empty++; + } + else if (ending) { + break; + } + i--; + } + return empty; + }; + + /** + * Return true if the row at the given index is empty, false otherwise + * @param {Number} r Row index + * @return {Boolean} + */ + this.isEmptyRow = function (r) { + if (priv.settings.isEmptyRow) { + return priv.settings.isEmptyRow.call(this, r); + } + + var val; + for (var c = 0, clen = this.countCols(); c < clen; c++) { + val = this.getDataAtCell(r, c); + if (val !== '' && val !== null && typeof val !== 'undefined') { + return false; + } + } + return true; + }; + + /** + * Return true if the column at the given index is empty, false otherwise + * @param {Number} c Column index + * @return {Boolean} + */ + this.isEmptyCol = function (c) { + if (priv.settings.isEmptyCol) { + return priv.settings.isEmptyCol.call(this, c); + } + + var val; + for (var r = 0, rlen = this.countRows(); r < rlen; r++) { + val = this.getDataAtCell(r, c); + if (val !== '' && val !== null && typeof val !== 'undefined') { + return false; + } + } + return true; + }; + /** * Selects cell on grid. Optionally selects range to another cell * @param {Number} row @@ -2294,6 +2520,7 @@ Handsontable.Core = function (rootElement, settings) { * @param {Number} [endCol] * @param {Boolean} [scrollToCell=true] If true, viewport will be scrolled to the selection * @public + * @return {Boolean} */ this.selectCell = function (row, col, endRow, endCol, scrollToCell) { if (typeof row !== 'number' || row < 0 || row >= self.countRows()) { @@ -2311,12 +2538,16 @@ Handsontable.Core = function (rootElement, settings) { } } priv.selStart.coords({row: row, col: col}); + self.$table[0].focus(); //needed or otherwise prepare won't focus the cell. selectionSpec tests this (should move focus to selected cell) if (typeof endRow === "undefined") { selection.setRangeEnd({row: row, col: col}, scrollToCell); } else { selection.setRangeEnd({row: endRow, col: endCol}, scrollToCell); } + + self.selection.finish(); + return true; }; this.selectCellByProp = function (row, prop, endRow, endProp, scrollToCell) { @@ -2340,19 +2571,51 @@ Handsontable.Core = function (rootElement, settings) { * @public */ this.destroy = function () { + self.clearTimeouts(); + if (self.view) { //in case HT is destroyed before initialization has finished + self.view.wt.destroy(); + } self.rootElement.empty(); self.rootElement.removeData('handsontable'); self.rootElement.off('.handsontable'); + $(window).off('.' + self.guid); + $(document.documentElement).off('.' + self.guid); + Handsontable.PluginHooks.run(self, 'afterDestroy'); + }; + + this.timeouts = {}; + + /** + * Sets timeout. Purpose of this method is to clear all known timeouts when `destroy` method is called + * @public + */ + this.registerTimeout = function (key, handle, ms) { + clearTimeout(this.timeouts[key]); + this.timeouts[key] = setTimeout(handle, ms || 0); + }; + + /** + * Clears all known timeouts + * @public + */ + this.clearTimeouts = function () { + for (var key in this.timeouts) { + if (this.timeouts.hasOwnProperty(key)) { + clearTimeout(this.timeouts[key]); + } + } }; /** * Handsontable version */ - this.version = '0.8.8'; //inserted by grunt from package.json + this.version = '0.8.16'; //inserted by grunt from package.json }; var settings = { 'data': void 0, + 'width': void 0, + 'height': void 0, 'startRows': 5, 'startCols': 5, 'minRows': 0, @@ -2375,7 +2638,9 @@ var settings = { 'currentRowClassName': void 0, 'currentColClassName': void 0, 'asyncRendering': true, - 'stretchH': 'hybrid' + 'stretchH': 'hybrid', + isEmptyRow: void 0, + isEmptyCol: void 0 }; $.fn.handsontable = function (action) { @@ -2428,6 +2693,7 @@ Handsontable.TableView = function (instance) { this.instance = instance; var settings = this.instance.getSettings(); + instance.rootElement.data('originalStyle', instance.rootElement.attr('style')); //needed to retrieve original style in jsFiddle link generator in HT examples. may be removed in future versions instance.rootElement.addClass('handsontable'); var $table = $('
'); @@ -2446,10 +2712,20 @@ Handsontable.TableView = function (instance) { //instance.rootElement[0].style.height = ''; //instance.rootElement[0].style.width = ''; + $(document.documentElement).on('keyup.' + instance.guid, function (event) { + if (instance.selection.isInProgress() && !event.shiftKey) { + instance.selection.finish(); + } + }); + var isMouseDown , dragInterval; - $(document.body).on('mouseup', function () { + $(document.documentElement).on('mouseup.' + instance.guid, function (event) { + if (instance.selection.isInProgress() && event.which === 1) { //is left mouse button + instance.selection.finish(); + } + isMouseDown = false; clearInterval(dragInterval); dragInterval = null; @@ -2462,11 +2738,12 @@ Handsontable.TableView = function (instance) { } }); - $(document.documentElement).on('mousedown', function (event) { + $(document.documentElement).on('mousedown.' + instance.guid, function (event) { var next = event.target; + if (next !== that.wt.wtTable.spreader) { //immediate click on "spreader" means click on the right side of vertical scrollbar while (next !== null && next !== document.documentElement) { - if (next === instance.rootElement[0] || $(next).attr('id') === 'context-menu-layer' || $(next).is('.context-menu-list') || $(next).is('.typeahead li')) { + if (next === instance.rootElement[0] || next.id === 'context-menu-layer' || $(next).is('.context-menu-list') || $(next).is('.typeahead li')) { return; //click inside container } next = next.parentNode; @@ -2622,6 +2899,10 @@ Handsontable.TableView = function (instance) { TD.focus(); event.preventDefault(); clearTextSelection(); + + if (settings.afterOnCellMouseDown) { + settings.afterOnCellMouseDown.call(that.instance, event, coords, TD); + } }, onCellMouseOver: function (event, coords, TD) { var coordsObj = {row: coords[0], col: coords[1]}; @@ -2637,11 +2918,8 @@ Handsontable.TableView = function (instance) { instance.autofill.handle.isDragged = 1; event.preventDefault(); }, - onCellCornerDblClick: function (event) { + onCellCornerDblClick: function () { instance.autofill.selectAdjacent(); - }, - onDraw: function (event) { - $window.trigger('resize'); } }; @@ -2651,18 +2929,22 @@ Handsontable.TableView = function (instance) { this.instance.forceFullRender = true; //used when data was changed this.render(); - var resizeTimeout; - $window.on('resize', function () { - clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(function () { - var lastContainerWidth = that.containerWidth; - var lastContainerHeight = that.containerHeight; + var lastContainerWidth = that.containerWidth; + var lastContainerHeight = that.containerHeight; + + $window.on('resize.' + instance.guid, function () { + that.instance.registerTimeout('resizeTimeout', function () { that.determineContainerSize(); - if (lastContainerWidth !== that.containerWidth || lastContainerHeight !== that.containerHeight) { - that.wt.update('width', that.containerWidth); - that.wt.update('height', that.containerHeight); + var newContainerWidth = that.containerWidth; + var newContainerHeight = that.containerHeight; + + if (lastContainerWidth !== newContainerWidth || lastContainerHeight !== newContainerHeight) { + that.wt.update('width', newContainerWidth); + that.wt.update('height', newContainerHeight); that.instance.forceFullRender = true; that.render(); + lastContainerWidth = newContainerWidth; + lastContainerHeight = newContainerHeight; } }, 60); }); @@ -2677,16 +2959,18 @@ Handsontable.TableView = function (instance) { }; Handsontable.TableView.prototype.isCellEdited = function () { - return (this.instance.textEditor && this.instance.textEditor.isCellEdited) || (this.instance.autocompleteEditor && this.instance.autocompleteEditor.isCellEdited); + return (this.instance.textEditor && this.instance.textEditor.isCellEdited) || (this.instance.autocompleteEditor && this.instance.autocompleteEditor.isCellEdited) || (this.instance.handsontableEditor && this.instance.handsontableEditor.isCellEdited); }; Handsontable.TableView.prototype.determineContainerSize = function () { var settings = this.instance.getSettings(); - this.containerWidth = settings.width; - this.containerHeight = settings.height; + + this.containerWidth = typeof settings.width === 'function' ? settings.width() : settings.width; + this.containerHeight = typeof settings.height === 'function' ? settings.height() : settings.height; var computedWidth = this.instance.rootElement.width(); var computedHeight = this.instance.rootElement.height(); + if (settings.width === void 0 && computedWidth > 0) { this.containerWidth = computedWidth; } @@ -2695,6 +2979,14 @@ Handsontable.TableView.prototype.determineContainerSize = function () { if (settings.height === void 0 && computedHeight > 0) { this.containerHeight = computedHeight; } + + if (this.instance.rootElement[0].style.height === '') { + if (this.wt && this.wt.wtScroll.wtScrollbarV.visible) { + if (typeof this.containerHeight === 'number') { //TODO move this to Handsontable, then this typeof can be removed + this.containerHeight += this.wt.getSetting('scrollbarHeight'); + } + } + } } }; @@ -2714,7 +3006,7 @@ Handsontable.TableView.prototype.applyCellTypeMethod = function (methodName, td, var prop = this.instance.colToProp(col) , cellProperties = this.instance.getCellMeta(row, col); if (cellProperties[methodName]) { - return cellProperties[methodName](this.instance, td, row, col, prop, this.instance.getDataAtCell(row, col), cellProperties); + return cellProperties[methodName](this.instance, td, row, col, prop, this.instance.getDataAtRowProp(row, prop), cellProperties); } }; @@ -2783,6 +3075,49 @@ Handsontable.helper.stringify = function (value) { } }; +// Remove childs function +// WARNING - this doesn't unload events and data attached by jQuery +// http://jsperf.com/jquery-html-vs-empty-vs-innerhtml/9 +Handsontable.helper.empty = function (element) { + var child; + while (child = element.lastChild) { + element.removeChild(child); + } +}; + + +/** + * Checks if child is a descendant of given parent node + * http://stackoverflow.com/questions/2234979/how-to-check-in-javascript-if-one-element-is-a-child-of-another + * @param parent + * @param child + * @returns {boolean} + */ +Handsontable.helper.isDescendant = function (parent, child) { + var node = child.parentNode; + while (node != null) { + if (node == parent) { + return true; + } + node = node.parentNode; + } + return false; +}; + +/** + * Generates a random hex string. Used as namespace for Handsontable instance events. + * @return {String} - 16 character random string: "92b1bfc74ec4" + */ +Handsontable.helper.randomString = function () { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + }; + + return s4() + s4() + s4() + s4(); +}; + /** * Handsontable UndoRedo class */ @@ -2807,7 +3142,7 @@ Handsontable.UndoRedo.prototype.undo = function () { for (i = 0, ilen = setData.length; i < ilen; i++) { setData[i].splice(3, 1); } - this.instance.setDataAtCell(setData, null, null, 'undo'); + this.instance.setDataAtRowProp(setData, null, null, 'undo'); this.rev--; } }; @@ -2823,7 +3158,7 @@ Handsontable.UndoRedo.prototype.redo = function () { for (i = 0, ilen = setData.length; i < ilen; i++) { setData[i].splice(2, 1); } - this.instance.setDataAtCell(setData, null, null, 'redo'); + this.instance.setDataAtRowProp(setData, null, null, 'redo'); } }; @@ -2974,13 +3309,13 @@ Handsontable.CheckboxRenderer = function (instance, td, row, col, prop, value, c td.innerHTML = "#bad value#"; } - var $input = $(td).find('input:first'); + var $input = $(td.getElementsByTagName('input')[0]); $input.mousedown(function (event) { - if (!$(this).is(':checked')) { - instance.setDataAtCell(row, prop, cellProperties.checkedTemplate); + if (!this.checked) { + instance.setDataAtRowProp(row, prop, cellProperties.checkedTemplate); } else { - instance.setDataAtCell(row, prop, cellProperties.uncheckedTemplate); + instance.setDataAtRowProp(row, prop, cellProperties.uncheckedTemplate); } event.stopPropagation(); //otherwise can confuse mousedown handler }); @@ -3014,47 +3349,40 @@ Handsontable.NumericRenderer = function (instance, td, row, col, prop, value, ce } }; function HandsontableTextEditorClass(instance) { - this.isCellEdited = false; - this.instance = instance; - this.originalValue = ''; - this.row; - this.col; - this.prop; - - this.createElements(); - - /*instance.that.TEXTAREA.on('blur.editor', function () { - if (that.isCellEdited) { - that.finishEditing(false); - } - });*/ - - this.bindEvents(); + if (instance) { + this.isCellEdited = false; + this.instance = instance; + this.createElements(); + this.bindEvents(); + } } HandsontableTextEditorClass.prototype.createElements = function () { + + var style; + this.TEXTAREA = $('').val(o.value||"").appendTo(a),o.height&&l.height(o.height);break;case"checkbox":l=t('').val(o.value||"").prop("checked",!!o.selected).prependTo(a);break;case"radio":l=t('').val(o.value||"").prop("checked",!!o.selected).prependTo(a);break;case"select":l=t('":a===l.uncheckedTemplate||a===n.helper.stringify(l.uncheckedTemplate)?"":null===a?"":"#bad value#";var c=t(i.getElementsByTagName("input")[0]);return c.mousedown(function(t){this.checked?e.setDataAtRowProp(s,r,l.uncheckedTemplate):e.setDataAtRowProp(s,r,l.checkedTemplate),t.stopPropagation()}),c.mouseup(function(t){t.stopPropagation()}),i},n.NumericRenderer=function(t,e,i,s,o,r,a){"number"==typeof r?(a.language!==void 0&&numeral.language(a.language),e.innerHTML=numeral(r).format(a.format||"0"),e.className="htNumeric"):n.TextRenderer(t,e,i,s,o,r,a)},i.prototype.createElements=function(){var e;this.TEXTAREA=t('').val(o.value||"").appendTo(a),o.height&&l.height(o.height);break;case"checkbox":l=t('').val(o.value||"").prop("checked",!!o.selected).prependTo(a);break;case"radio":l=t('').val(o.value||"").prop("checked",!!o.selected).prependTo(a);break;case"select":l=t('

- + diff --git a/src/3rdparty/handsontable/jquery.handsontable.full.css b/src/3rdparty/handsontable/jquery.handsontable.full.css index 8e2f2e7c..ee2a5b0c 100644 --- a/src/3rdparty/handsontable/jquery.handsontable.full.css +++ b/src/3rdparty/handsontable/jquery.handsontable.full.css @@ -1,12 +1,12 @@ /** - * Handsontable 0.8.8 + * Handsontable 0.8.16 * 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) + * Date: Tue Mar 26 2013 02:34:10 GMT+0100 (Central European Standard Time) */ .handsontable { @@ -16,6 +16,13 @@ font-size: 13px; } +.handsontable.hidden { + display : none; + left : 0; + position :absolute; + top : 0; +} + .handsontable * { box-sizing: content-box; -webkit-box-sizing: content-box; @@ -37,6 +44,10 @@ table-layout: fixed; width: 0; outline-width: 0; + /* reset bootstrap table style. for more info see: https://github.com/warpech/jquery-handsontable/issues/224 */ + max-width : none; + max-height: none; +} } .handsontable col { @@ -192,6 +203,15 @@ textarea.handsontableInput { font-size: 13px; box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.4); resize: none; + + /*below are needed to overwrite stuff added by jQuery UI Bootstrap theme*/ + display: inline-block; + font-size: 13px; + line-height: inherit; + color: #000; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; } .handsontableInputHolder { @@ -233,7 +253,7 @@ NumericRenderer } /* typeahead rules. Needed only if you are using the autocomplete feature */ -.typeahead { +.handsontable .typeahead { position: absolute; font-family: Arial, Helvetica, sans-serif; line-height: 1.3em; @@ -264,28 +284,33 @@ NumericRenderer border-radius: 4px; } -.typeahead li { +.handsontable .typeahead li { line-height: 18px; + min-height: 18px; display: list-item; + margin: 0; } -.typeahead a { +.handsontable .typeahead a { display: block; padding: 3px 15px; clear: both; font-weight: normal; line-height: 18px; + min-height: 18px; color: #333; white-space: nowrap; } -.typeahead li > a:hover, .typeahead .active > a, .typeahead .active > a:hover { +.handsontable .typeahead li > a:hover, +.handsontable .typeahead .active > a, +.handsontable .typeahead .active > a:hover { color: white; text-decoration: none; background-color: #08C; } -.typeahead a { +.handsontable .typeahead a { color: #08C; text-decoration: none; } diff --git a/src/3rdparty/handsontable/jquery.handsontable.full.js b/src/3rdparty/handsontable/jquery.handsontable.full.js index b6f37d73..671ef11a 100644 --- a/src/3rdparty/handsontable/jquery.handsontable.full.js +++ b/src/3rdparty/handsontable/jquery.handsontable.full.js @@ -1,12 +1,12 @@ /** - * Handsontable 0.8.8 + * Handsontable 0.8.16 * 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) + * Date: Tue Mar 26 2013 02:34:10 GMT+0100 (Central European Standard Time) */ /*jslint white: true, browser: true, plusplus: true, indent: 4, maxerr: 50 */ @@ -25,8 +25,13 @@ var Handsontable = { //class namespace */ Handsontable.Core = function (rootElement, settings) { this.rootElement = rootElement; + this.guid = 'ht_' + Handsontable.helper.randomString(); //this is the namespace for global events - var priv, datamap, grid, selection, editproxy, autofill, validate, self = this; + if (!this.rootElement[0].id) { + this.rootElement[0].id = this.guid; //if root element does not have an id, assign a random id + } + + var priv, datamap, grid, selection, editproxy, autofill, self = this; priv = { settings: {}, @@ -142,44 +147,50 @@ Handsontable.Core = function (rootElement, settings) { /** * 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 + * @param {Number} [index] Optional. Index of the row before which the new row will be inserted */ - createRow: function (coords) { - var row; + createRow: function (index) { + var row + , rowCount = self.countRows(); + + if (typeof index !== 'number' || index >= rowCount) { + index = rowCount; + } + if (priv.dataType === 'array') { row = []; for (var c = 0, clen = self.countCols(); c < clen; c++) { row.push(null); } } + else if (priv.dataType === 'function') { + row = priv.settings.dataSchema(index); + } else { row = $.extend(true, {}, datamap.getSchema()); } - if (!coords || coords.row >= self.countRows()) { - if (priv.settings.onCreateRow) { - priv.settings.onCreateRow(self.countRows(), row); - } + if (priv.settings.onCreateRow) { + priv.settings.onCreateRow(index, row); + } + if (index === rowCount) { priv.settings.data.push(row); } else { - if (priv.settings.onCreateRow) { - priv.settings.onCreateRow(coords.row, row); - } - priv.settings.data.splice(coords.row, 0, row); + priv.settings.data.splice(index, 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 + * @param {Object} [index] Optional. Index of the column before which the new column will be inserted */ - createCol: function (coords) { + 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 = self.countRows(); - if (!coords || coords.col >= self.countCols()) { + if (typeof index !== 'number' || index >= self.countCols()) { for (; r < rlen; r++) { if (typeof priv.settings.data[r] === 'undefined') { priv.settings.data[r] = []; @@ -189,47 +200,45 @@ Handsontable.Core = function (rootElement, settings) { } else { for (; r < rlen; r++) { - priv.settings.data[r].splice(coords.col, 0, ''); + priv.settings.data[r].splice(index, 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 + * 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 (coords, toCoords) { - if (!coords || coords.row === self.countRows() - 1) { - priv.settings.data.pop(); + removeRow: function (index, amount) { + if (!amount) { + amount = 1; } - else { - priv.settings.data.splice(coords.row, toCoords.row - coords.row + 1); + if (typeof index !== 'number') { + index = -amount; } + priv.settings.data.splice(index, amount); 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 + * 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 (coords, toCoords) { + 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"); } - var r = 0; - if (!coords || coords.col === self.countCols() - 1) { - for (; r < self.countRows(); r++) { - priv.settings.data[r].pop(); - } + if (!amount) { + amount = 1; } - else { - var howMany = toCoords.col - coords.col + 1; - for (; r < self.countRows(); r++) { - priv.settings.data[r].splice(coords.col, howMany); - } + if (typeof index !== 'number') { + index = -amount; + } + for (var r = 0, rlen = self.countRows(); r < rlen; r++) { + priv.settings.data[r].splice(index, amount); } self.forceFullRender = true; //used when data was changed }, @@ -258,6 +267,25 @@ Handsontable.Core = function (rootElement, settings) { } 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; } @@ -283,6 +311,13 @@ Handsontable.Core = function (rootElement, settings) { } 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; } @@ -340,22 +375,29 @@ Handsontable.Core = function (rootElement, settings) { grid = { /** - * Alter grid + * Inserts or removes rows and columns * @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" + * @param {Number} index + * @param {Number} amount */ - alter: function (action, coords, toCoords) { - var oldData, newData, changes, r, rlen, c, clen; + alter: function (action, index, amount) { + var oldData, newData, changes, r, rlen, c, clen, delta; 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 + if (!amount) { + amount = 1; + } + delta = 0; + while (delta < amount && self.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 @@ -364,11 +406,18 @@ Handsontable.Core = function (rootElement, settings) { 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 + if (!amount) { + amount = 1; + } + delta = 0; + while (delta < amount && self.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 @@ -377,16 +426,20 @@ Handsontable.Core = function (rootElement, settings) { break; case "remove_row": - datamap.removeRow(coords, toCoords); + datamap.removeRow(index, amount); grid.keepEmptyRows(); selection.refreshBorders(); //it will call render and prepare methods break; case "remove_col": - datamap.removeCol(coords, toCoords); + datamap.removeCol(index, amount); grid.keepEmptyRows(); selection.refreshBorders(); //it will call render and prepare methods break; + + default: + throw Error('There is no such action "' + action + '"'); + break; } changes = []; @@ -404,20 +457,9 @@ Handsontable.Core = function (rootElement, settings) { * 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++; - } + var r, rlen, emptyRows = self.countEmptyRows(true), emptyCols; - //should I add empty rows to data source to meet startRows? + //should I add empty rows to data source to meet minRows? rlen = self.countRows(); if (rlen < priv.settings.minRows) { for (r = 0; r < priv.settings.minRows - rlen; r++) { @@ -433,17 +475,7 @@ Handsontable.Core = function (rootElement, settings) { } //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++; - } - } + emptyCols = self.countEmptyCols(true); //should I add empty cols to meet minCols? if (!priv.settings.columns && self.countCols() < priv.settings.minCols) { @@ -548,8 +580,7 @@ Handsontable.Core = function (rootElement, settings) { break; } if (self.getCellMeta(current.row, current.col).isWritable) { - var p = datamap.colToProp(current.col); - setData.push([current.row, p, input[r][c]]); + setData.push([current.row, current.col, input[r][c]]); } current.col++; if (end && c === clen - 1) { @@ -612,12 +643,34 @@ Handsontable.Core = function (rootElement, settings) { }; 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 () { + self.selection.inProgress = true; + }, + + /** + * Sets inProgress to false. Triggers onSelectionEnd and onSelectionEndByProp + */ + finish: function () { + var sel = self.getSelected(); + self.rootElement.triggerHandler("selectionend.handsontable", sel); + self.rootElement.triggerHandler("selectionendbyprop.handsontable", [sel[0], self.colToProp(sel[1]), sel[2], self.colToProp(sel[3])]); + self.selection.inProgress = false; + }, + + isInProgress: function () { + return self.selection.inProgress; + }, + /** * Starts selection range on given td object * @param {Object} coords */ setRangeStart: function (coords) { - selection.deselect(); priv.selStart.coords(coords); selection.setRangeEnd(coords); }, @@ -628,6 +681,8 @@ Handsontable.Core = function (rootElement, settings) { * @param {Boolean} [scrollToCell=true] If true, viewport will be scrolled to range end */ setRangeEnd: function (coords, scrollToCell) { + self.selection.begin(); + priv.selEnd.coords(coords); if (!priv.settings.multiSelect) { priv.selStart.coords(coords); @@ -791,6 +846,7 @@ Handsontable.Core = function (rootElement, settings) { if (!selection.isSelected()) { return; } + self.selection.inProgress = false; //needed by HT inception 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(); @@ -828,7 +884,7 @@ Handsontable.Core = function (rootElement, settings) { 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), '']); + changes.push([r, c, '']); } } } @@ -1021,6 +1077,10 @@ Handsontable.Core = function (rootElement, settings) { var $body = $(document.body); function onKeyDown(event) { + if (priv.settings.beforeOnKeyDown) { + priv.settings.beforeOnKeyDown.call(self, event); + } + if ($body.children('.context-menu-list:visible').length) { return; } @@ -1061,6 +1121,7 @@ Handsontable.Core = function (rootElement, settings) { selection.transformStart(-1, 0); } event.preventDefault(); + event.stopPropagation(); //required by HandsontableEditor break; case 9: /* tab */ @@ -1072,6 +1133,7 @@ Handsontable.Core = function (rootElement, settings) { 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 */ @@ -1082,6 +1144,7 @@ Handsontable.Core = function (rootElement, settings) { selection.transformStart(0, 1); } event.preventDefault(); + event.stopPropagation(); //required by HandsontableEditor break; case 37: /* arrow left */ @@ -1092,6 +1155,7 @@ Handsontable.Core = function (rootElement, settings) { selection.transformStart(0, -1); } event.preventDefault(); + event.stopPropagation(); //required by HandsontableEditor break; case 8: /* backspace */ @@ -1108,6 +1172,7 @@ Handsontable.Core = function (rootElement, settings) { selection.transformStart(1, 0); //move selection down } event.preventDefault(); + event.stopPropagation(); //required by HandsontableEditor break; case 113: /* F2 */ @@ -1132,6 +1197,7 @@ Handsontable.Core = function (rootElement, settings) { else { rangeModifier({row: priv.selStart.row(), col: 0}); } + event.stopPropagation(); //required by HandsontableEditor break; case 35: /* end */ @@ -1141,6 +1207,7 @@ Handsontable.Core = function (rootElement, settings) { else { rangeModifier({row: priv.selStart.row(), col: self.countCols() - 1}); } + event.stopPropagation(); //required by HandsontableEditor break; case 33: /* pg up */ @@ -1148,6 +1215,7 @@ Handsontable.Core = function (rootElement, settings) { self.view.wt.scrollVertical(-self.countVisibleRows()); self.view.render(); event.preventDefault(); //don't page up the window + event.stopPropagation(); //required by HandsontableEditor break; case 34: /* pg down */ @@ -1155,6 +1223,7 @@ Handsontable.Core = function (rootElement, settings) { self.view.wt.scrollVertical(self.countVisibleRows()); self.view.render(); event.preventDefault(); //don't page down the window + event.stopPropagation(); //required by HandsontableEditor break; default: @@ -1166,7 +1235,7 @@ Handsontable.Core = function (rootElement, settings) { self.copyPaste = new CopyPaste(self.rootElement[0]); self.copyPaste.onCut(onCut); self.copyPaste.onPaste(onPaste); - self.rootElement.on('keydown.handsontable', onKeyDown); + self.rootElement.on('keydown.handsontable.' + self.guid, onKeyDown); }, /** @@ -1175,8 +1244,9 @@ Handsontable.Core = function (rootElement, settings) { */ destroy: function (revertOriginal) { if (typeof priv.editorDestroyer === "function") { - priv.editorDestroyer(revertOriginal); + var destroyer = priv.editorDestroyer; //this copy is needed, otherwise destroyer can enter an infinite loop priv.editorDestroyer = null; + destroyer(revertOriginal); } }, @@ -1202,17 +1272,26 @@ Handsontable.Core = function (rootElement, settings) { * Prepare text input to be displayed at given grid cell */ prepare: function () { + if (!self.getCellMeta(priv.selStart.row(), priv.selStart.col()).isWritable) { + return; + } + if (priv.settings.asyncRendering) { - clearTimeout(window.prepareFrame); - window.prepareFrame = setTimeout(function () { + self.registerTimeout('prepareFrame', function () { var TD = self.view.getCellAtCoords(priv.selStart.coords()); - TD.focus(); + if (Handsontable.helper.isDescendant(self.rootElement[0], document.activeElement)) { + //we don't want to steal focus if it is outside HT (issue #408) + 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(); + if (Handsontable.helper.isDescendant(self.rootElement[0], document.activeElement)) { + //we don't want to steal focus if it is outside HT (issue #408) + TD.focus(); + } priv.editorDestroyer = self.view.applyCellTypeMethod('editor', TD, priv.selStart.row(), priv.selStart.col()); } } @@ -1233,60 +1312,56 @@ Handsontable.Core = function (rootElement, settings) { Handsontable.PluginHooks.run(self, 'afterInit'); }; - validate = function (changes, source) { + function validateChanges(changes, source) { var validated = $.Deferred(); var deferreds = []; - if (source === 'paste') { - //validate strict autocompletes - var process = function (i) { - var deferred = $.Deferred(); - deferreds.push(deferred); + //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; + 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; - } + 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; } - if (!found) { - changes[i] = null; + else if (lowercaseVal === source[s].toLowerCase()) { + changes[i][3] = source[s]; //good match, fix the case + found = true; + break; } - 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) - } + 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) { + $.isFunction(cellProperties.source) ? cellProperties.source(changes[i][3], process(i)) : process(i)(cellProperties.source); } } - $.when(deferreds).then(function () { + $.when.apply($, deferreds).then(function () { for (var i = changes.length - 1; i >= 0; i--) { if (changes[i] === null) { changes.splice(i, 1); - } + } else { + var cellProperties = self.getCellMeta(changes[i][0], datamap.propToCol(changes[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 (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 + } } } } @@ -1311,11 +1386,11 @@ Handsontable.Core = function (rootElement, settings) { }); return $.when(validated); - }; + } var fireEvent = function (name, params) { if (priv.settings.asyncRendering) { - setTimeout(function () { + self.registerTimeout('fireEvent', function () { self.rootElement.triggerHandler(name, params); }, 0); } @@ -1340,58 +1415,124 @@ Handsontable.Core = function (rootElement, settings) { priv.settings.onSelectionByProp.apply(self.rootElement[0], [row, prop, endRow, endProp]); } }); + self.rootElement.on("selectionend.handsontable", function (event, row, col, endRow, endCol) { + if (priv.settings.onSelectionEnd) { + priv.settings.onSelectionEnd.apply(self.rootElement[0], [row, col, endRow, endCol]); + } + }); + self.rootElement.on("selectionendbyprop.handsontable", function (event, row, prop, endRow, endProp) { + if (priv.settings.onSelectionEndByProp) { + priv.settings.onSelectionEndByProp.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) + * Internal function to apply changes. Called after validateChanges + * @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) */ - this.setDataAtCell = function (row, prop, value, source) { - var changes, i, ilen; + function applyChanges(changes, source) { + var i = 0 + , ilen = changes.length; - if (typeof row === "object") { //is it an array of changes - changes = row; + if (!ilen) { + return; + } + + while (i < ilen) { + if (priv.settings.minSpareRows) { + while (changes[i][0] > self.countRows() - 1) { + datamap.createRow(); + } + } + if (priv.dataType === 'array' && priv.settings.minSpareCols) { + while (datamap.propToCol(changes[i][1]) > self.countCols() - 1) { + datamap.createCol(); + } + } + datamap.set(changes[i][0], changes[i][1], changes[i][3]); + i++; } - else if ($.isPlainObject(value)) { //backwards compatibility - changes = value; + self.forceFullRender = true; //used when data was changed + grid.keepEmptyRows(); + selection.refreshBorders(); + fireEvent("datachange.handsontable", [changes, source || 'edit']); + } + + function setDataInputToArray(arg0, arg1, arg2) { + if (typeof arg0 === "object") { //is it an array of changes + return arg0; + } + else if ($.isPlainObject(arg2)) { //backwards compatibility + return value; } else { - changes = [ - [row, prop, value] + return [ + [arg0, arg1, arg2] ]; } + } + + /** + * Set data at given cell + * @public + * @param {Number|Array} row or array of changes in format [[row, col, value], ...] + * @param {Number} col + * @param {String} value + * @param {String} source String that identifies how this change will be described in changes array (useful in onChange callback) + */ + this.setDataAtCell = function (row, col, value, source) { + var input = setDataInputToArray(row, col, value) + , i + , ilen + , changes = [] + , prop; - 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 + for (i = 0, ilen = input.length; i < ilen; i++) { + 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, + datamap.get(input[i][0], prop), + input[i][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]; + validateChanges(changes, source).then(function () { + applyChanges(changes, source); + }); + }; - 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']); + + /** + * Set data at given row property + * @public + * @param {Number|Array} row or array of changes in format [[row, prop, value], ...] + * @param {Number} prop + * @param {String} value + * @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], + datamap.get(input[i][0], input[i][1]), + input[i][2] + ]); + } + + validateChanges(changes, source).then(function () { + applyChanges(changes, source); }); }; @@ -1427,12 +1568,11 @@ Handsontable.Core = function (rootElement, settings) { /** * Returns current selection. Returns undefined if there is no selection. * @public - * @return {Array} [topLeftRow, topLeftCol, bottomRightRow, bottomRightCol] + * @return {Array} [`startRow`, `startCol`, `endRow`, `endCol`] */ 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]; + return [priv.selStart.row(), priv.selStart.col(), priv.selEnd.row(), priv.selEnd.col()]; } }; @@ -1453,13 +1593,20 @@ Handsontable.Core = function (rootElement, settings) { * @param {Array} data */ this.loadData = function (data) { + if (!(data instanceof Array)) { + throw new Error("loadData only accepts array of objects or array of arrays (" + typeof data + " given)"); + } + priv.isPopulated = false; priv.settings.data = data; - if ($.isPlainObject(priv.settings.dataSchema) || $.isPlainObject(data[0])) { - priv.dataType = 'object'; + if (priv.settings.dataSchema instanceof Array || data[0] instanceof Array) { + priv.dataType = 'array'; + } + else if ($.isFunction(priv.settings.dataSchema)) { + priv.dataType = 'function'; } else { - priv.dataType = 'array'; + priv.dataType = 'object'; } if (data[0]) { priv.duckDataSchema = datamap.recursiveDuckSchema(data[0]); @@ -1631,31 +1778,14 @@ Handsontable.Core = function (rootElement, settings) { }; /** - * Alters the grid + * Inserts or removes rows and columns * @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" + * @param {Number} index + * @param {Number} amount * @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; - } + this.alter = function (action, index, amount) { + grid.alter(action, index, amount); }; /** @@ -1690,16 +1820,27 @@ Handsontable.Core = function (rootElement, settings) { }; /** - * Return cell value at `row`, `col` + * Return value at `row`, `col` * @param {Number} row * @param {Number} col * @public - * @return {string} + * @return value (mixed data type) */ this.getDataAtCell = function (row, col) { return datamap.get(row, datamap.colToProp(col)); }; + /** + * Return value at `row`, `prop` + * @param {Number} row + * @param {Number} prop + * @public + * @return value (mixed data type) + */ + this.getDataAtRowProp = function (row, prop) { + return datamap.get(row, prop); + }; + /** * Returns cell meta data object corresponding to params row, col * @param {Number} row @@ -1786,7 +1927,7 @@ Handsontable.Core = function (rootElement, settings) { while (dividend > 0) { modulo = (dividend - 1) % 26; columnLabel = String.fromCharCode(65 + modulo) + columnLabel; - dividend = parseInt((dividend - modulo) / 26); + dividend = parseInt((dividend - modulo) / 26, 10); } DIV.innerHTML = '' + columnLabel + ''; } @@ -1835,7 +1976,7 @@ Handsontable.Core = function (rootElement, settings) { * @return {Number} */ this.countCols = function () { - if (priv.dataType === 'object') { + if (priv.dataType === 'object' || priv.dataType === 'function') { if (priv.settings.columns && priv.settings.columns.length) { return priv.settings.columns.length; } @@ -1847,8 +1988,11 @@ Handsontable.Core = function (rootElement, settings) { if (priv.settings.columns && priv.settings.columns.length) { return priv.settings.columns.length; } + else if (priv.settings.data && priv.settings.data[0] && priv.settings.data[0].length) { + return priv.settings.data[0].length; + } else { - return Math.max((priv.settings.columns && priv.settings.columns.length) || 0, (priv.settings.data && priv.settings.data[0] && priv.settings.data[0].length) || 0); + return 0; } } }; @@ -1885,6 +2029,88 @@ Handsontable.Core = function (rootElement, settings) { return self.view.wt.getSetting('viewportColumns'); }; + /** + * Return number of empty rows + * @return {Boolean} ending If true, will only count empty rows at the end of the data source + */ + this.countEmptyRows = function (ending) { + var i = self.countRows() - 1 + , empty = 0; + while (i >= 0) { + if (self.isEmptyRow(i)) { + empty++; + } + else if (ending) { + break; + } + i--; + } + return empty; + }; + + /** + * Return number of empty columns + * @return {Boolean} ending If true, will only count empty columns at the end of the data source row + */ + this.countEmptyCols = function (ending) { + if (self.countRows() < 1) { + return 0; + } + + var i = self.countCols() - 1 + , empty = 0; + while (i >= 0) { + if (self.isEmptyCol(i)) { + empty++; + } + else if (ending) { + break; + } + i--; + } + return empty; + }; + + /** + * Return true if the row at the given index is empty, false otherwise + * @param {Number} r Row index + * @return {Boolean} + */ + this.isEmptyRow = function (r) { + if (priv.settings.isEmptyRow) { + return priv.settings.isEmptyRow.call(this, r); + } + + var val; + for (var c = 0, clen = this.countCols(); c < clen; c++) { + val = this.getDataAtCell(r, c); + if (val !== '' && val !== null && typeof val !== 'undefined') { + return false; + } + } + return true; + }; + + /** + * Return true if the column at the given index is empty, false otherwise + * @param {Number} c Column index + * @return {Boolean} + */ + this.isEmptyCol = function (c) { + if (priv.settings.isEmptyCol) { + return priv.settings.isEmptyCol.call(this, c); + } + + var val; + for (var r = 0, rlen = this.countRows(); r < rlen; r++) { + val = this.getDataAtCell(r, c); + if (val !== '' && val !== null && typeof val !== 'undefined') { + return false; + } + } + return true; + }; + /** * Selects cell on grid. Optionally selects range to another cell * @param {Number} row @@ -1893,6 +2119,7 @@ Handsontable.Core = function (rootElement, settings) { * @param {Number} [endCol] * @param {Boolean} [scrollToCell=true] If true, viewport will be scrolled to the selection * @public + * @return {Boolean} */ this.selectCell = function (row, col, endRow, endCol, scrollToCell) { if (typeof row !== 'number' || row < 0 || row >= self.countRows()) { @@ -1910,12 +2137,16 @@ Handsontable.Core = function (rootElement, settings) { } } priv.selStart.coords({row: row, col: col}); + self.$table[0].focus(); //needed or otherwise prepare won't focus the cell. selectionSpec tests this (should move focus to selected cell) if (typeof endRow === "undefined") { selection.setRangeEnd({row: row, col: col}, scrollToCell); } else { selection.setRangeEnd({row: endRow, col: endCol}, scrollToCell); } + + self.selection.finish(); + return true; }; this.selectCellByProp = function (row, prop, endRow, endProp, scrollToCell) { @@ -1939,19 +2170,51 @@ Handsontable.Core = function (rootElement, settings) { * @public */ this.destroy = function () { + self.clearTimeouts(); + if (self.view) { //in case HT is destroyed before initialization has finished + self.view.wt.destroy(); + } self.rootElement.empty(); self.rootElement.removeData('handsontable'); self.rootElement.off('.handsontable'); + $(window).off('.' + self.guid); + $(document.documentElement).off('.' + self.guid); + Handsontable.PluginHooks.run(self, 'afterDestroy'); + }; + + this.timeouts = {}; + + /** + * Sets timeout. Purpose of this method is to clear all known timeouts when `destroy` method is called + * @public + */ + this.registerTimeout = function (key, handle, ms) { + clearTimeout(this.timeouts[key]); + this.timeouts[key] = setTimeout(handle, ms || 0); + }; + + /** + * Clears all known timeouts + * @public + */ + this.clearTimeouts = function () { + for (var key in this.timeouts) { + if (this.timeouts.hasOwnProperty(key)) { + clearTimeout(this.timeouts[key]); + } + } }; /** * Handsontable version */ - this.version = '0.8.8'; //inserted by grunt from package.json + this.version = '0.8.16'; //inserted by grunt from package.json }; var settings = { 'data': void 0, + 'width': void 0, + 'height': void 0, 'startRows': 5, 'startCols': 5, 'minRows': 0, @@ -1974,7 +2237,9 @@ var settings = { 'currentRowClassName': void 0, 'currentColClassName': void 0, 'asyncRendering': true, - 'stretchH': 'hybrid' + 'stretchH': 'hybrid', + isEmptyRow: void 0, + isEmptyCol: void 0 }; $.fn.handsontable = function (action) { @@ -2027,6 +2292,7 @@ Handsontable.TableView = function (instance) { this.instance = instance; var settings = this.instance.getSettings(); + instance.rootElement.data('originalStyle', instance.rootElement.attr('style')); //needed to retrieve original style in jsFiddle link generator in HT examples. may be removed in future versions instance.rootElement.addClass('handsontable'); var $table = $('
'); @@ -2045,10 +2311,20 @@ Handsontable.TableView = function (instance) { //instance.rootElement[0].style.height = ''; //instance.rootElement[0].style.width = ''; + $(document.documentElement).on('keyup.' + instance.guid, function (event) { + if (instance.selection.isInProgress() && !event.shiftKey) { + instance.selection.finish(); + } + }); + var isMouseDown , dragInterval; - $(document.body).on('mouseup', function () { + $(document.documentElement).on('mouseup.' + instance.guid, function (event) { + if (instance.selection.isInProgress() && event.which === 1) { //is left mouse button + instance.selection.finish(); + } + isMouseDown = false; clearInterval(dragInterval); dragInterval = null; @@ -2061,11 +2337,12 @@ Handsontable.TableView = function (instance) { } }); - $(document.documentElement).on('mousedown', function (event) { + $(document.documentElement).on('mousedown.' + instance.guid, function (event) { var next = event.target; + if (next !== that.wt.wtTable.spreader) { //immediate click on "spreader" means click on the right side of vertical scrollbar while (next !== null && next !== document.documentElement) { - if (next === instance.rootElement[0] || $(next).attr('id') === 'context-menu-layer' || $(next).is('.context-menu-list') || $(next).is('.typeahead li')) { + if (next === instance.rootElement[0] || next.id === 'context-menu-layer' || $(next).is('.context-menu-list') || $(next).is('.typeahead li')) { return; //click inside container } next = next.parentNode; @@ -2221,6 +2498,10 @@ Handsontable.TableView = function (instance) { TD.focus(); event.preventDefault(); clearTextSelection(); + + if (settings.afterOnCellMouseDown) { + settings.afterOnCellMouseDown.call(that.instance, event, coords, TD); + } }, onCellMouseOver: function (event, coords, TD) { var coordsObj = {row: coords[0], col: coords[1]}; @@ -2236,11 +2517,8 @@ Handsontable.TableView = function (instance) { instance.autofill.handle.isDragged = 1; event.preventDefault(); }, - onCellCornerDblClick: function (event) { + onCellCornerDblClick: function () { instance.autofill.selectAdjacent(); - }, - onDraw: function (event) { - $window.trigger('resize'); } }; @@ -2250,18 +2528,22 @@ Handsontable.TableView = function (instance) { this.instance.forceFullRender = true; //used when data was changed this.render(); - var resizeTimeout; - $window.on('resize', function () { - clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(function () { - var lastContainerWidth = that.containerWidth; - var lastContainerHeight = that.containerHeight; + var lastContainerWidth = that.containerWidth; + var lastContainerHeight = that.containerHeight; + + $window.on('resize.' + instance.guid, function () { + that.instance.registerTimeout('resizeTimeout', function () { that.determineContainerSize(); - if (lastContainerWidth !== that.containerWidth || lastContainerHeight !== that.containerHeight) { - that.wt.update('width', that.containerWidth); - that.wt.update('height', that.containerHeight); + var newContainerWidth = that.containerWidth; + var newContainerHeight = that.containerHeight; + + if (lastContainerWidth !== newContainerWidth || lastContainerHeight !== newContainerHeight) { + that.wt.update('width', newContainerWidth); + that.wt.update('height', newContainerHeight); that.instance.forceFullRender = true; that.render(); + lastContainerWidth = newContainerWidth; + lastContainerHeight = newContainerHeight; } }, 60); }); @@ -2276,16 +2558,18 @@ Handsontable.TableView = function (instance) { }; Handsontable.TableView.prototype.isCellEdited = function () { - return (this.instance.textEditor && this.instance.textEditor.isCellEdited) || (this.instance.autocompleteEditor && this.instance.autocompleteEditor.isCellEdited); + return (this.instance.textEditor && this.instance.textEditor.isCellEdited) || (this.instance.autocompleteEditor && this.instance.autocompleteEditor.isCellEdited) || (this.instance.handsontableEditor && this.instance.handsontableEditor.isCellEdited); }; Handsontable.TableView.prototype.determineContainerSize = function () { var settings = this.instance.getSettings(); - this.containerWidth = settings.width; - this.containerHeight = settings.height; + + this.containerWidth = typeof settings.width === 'function' ? settings.width() : settings.width; + this.containerHeight = typeof settings.height === 'function' ? settings.height() : settings.height; var computedWidth = this.instance.rootElement.width(); var computedHeight = this.instance.rootElement.height(); + if (settings.width === void 0 && computedWidth > 0) { this.containerWidth = computedWidth; } @@ -2294,6 +2578,14 @@ Handsontable.TableView.prototype.determineContainerSize = function () { if (settings.height === void 0 && computedHeight > 0) { this.containerHeight = computedHeight; } + + if (this.instance.rootElement[0].style.height === '') { + if (this.wt && this.wt.wtScroll.wtScrollbarV.visible) { + if (typeof this.containerHeight === 'number') { //TODO move this to Handsontable, then this typeof can be removed + this.containerHeight += this.wt.getSetting('scrollbarHeight'); + } + } + } } }; @@ -2313,7 +2605,7 @@ Handsontable.TableView.prototype.applyCellTypeMethod = function (methodName, td, var prop = this.instance.colToProp(col) , cellProperties = this.instance.getCellMeta(row, col); if (cellProperties[methodName]) { - return cellProperties[methodName](this.instance, td, row, col, prop, this.instance.getDataAtCell(row, col), cellProperties); + return cellProperties[methodName](this.instance, td, row, col, prop, this.instance.getDataAtRowProp(row, prop), cellProperties); } }; @@ -2382,6 +2674,49 @@ Handsontable.helper.stringify = function (value) { } }; +// Remove childs function +// WARNING - this doesn't unload events and data attached by jQuery +// http://jsperf.com/jquery-html-vs-empty-vs-innerhtml/9 +Handsontable.helper.empty = function (element) { + var child; + while (child = element.lastChild) { + element.removeChild(child); + } +}; + + +/** + * Checks if child is a descendant of given parent node + * http://stackoverflow.com/questions/2234979/how-to-check-in-javascript-if-one-element-is-a-child-of-another + * @param parent + * @param child + * @returns {boolean} + */ +Handsontable.helper.isDescendant = function (parent, child) { + var node = child.parentNode; + while (node != null) { + if (node == parent) { + return true; + } + node = node.parentNode; + } + return false; +}; + +/** + * Generates a random hex string. Used as namespace for Handsontable instance events. + * @return {String} - 16 character random string: "92b1bfc74ec4" + */ +Handsontable.helper.randomString = function () { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + }; + + return s4() + s4() + s4() + s4(); +}; + /** * Handsontable UndoRedo class */ @@ -2406,7 +2741,7 @@ Handsontable.UndoRedo.prototype.undo = function () { for (i = 0, ilen = setData.length; i < ilen; i++) { setData[i].splice(3, 1); } - this.instance.setDataAtCell(setData, null, null, 'undo'); + this.instance.setDataAtRowProp(setData, null, null, 'undo'); this.rev--; } }; @@ -2422,7 +2757,7 @@ Handsontable.UndoRedo.prototype.redo = function () { for (i = 0, ilen = setData.length; i < ilen; i++) { setData[i].splice(2, 1); } - this.instance.setDataAtCell(setData, null, null, 'redo'); + this.instance.setDataAtRowProp(setData, null, null, 'redo'); } }; @@ -2573,13 +2908,13 @@ Handsontable.CheckboxRenderer = function (instance, td, row, col, prop, value, c td.innerHTML = "#bad value#"; } - var $input = $(td).find('input:first'); + var $input = $(td.getElementsByTagName('input')[0]); $input.mousedown(function (event) { - if (!$(this).is(':checked')) { - instance.setDataAtCell(row, prop, cellProperties.checkedTemplate); + if (!this.checked) { + instance.setDataAtRowProp(row, prop, cellProperties.checkedTemplate); } else { - instance.setDataAtCell(row, prop, cellProperties.uncheckedTemplate); + instance.setDataAtRowProp(row, prop, cellProperties.uncheckedTemplate); } event.stopPropagation(); //otherwise can confuse mousedown handler }); @@ -2613,47 +2948,40 @@ Handsontable.NumericRenderer = function (instance, td, row, col, prop, value, ce } }; function HandsontableTextEditorClass(instance) { - this.isCellEdited = false; - this.instance = instance; - this.originalValue = ''; - this.row; - this.col; - this.prop; - - this.createElements(); - - /*instance.that.TEXTAREA.on('blur.editor', function () { - if (that.isCellEdited) { - that.finishEditing(false); - } - });*/ - - this.bindEvents(); + if (instance) { + this.isCellEdited = false; + this.instance = instance; + this.createElements(); + this.bindEvents(); + } } HandsontableTextEditorClass.prototype.createElements = function () { + + var style; + this.TEXTAREA = $('