diff --git a/README.md b/README.md index d8900dd548b..5a4540a583b 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,9 @@ Maybe you want to show some default data. Use the `handsontable('loadData', data handsontable('alter', 'insert_col', index) | Method | Insert new column before the column at given index handsontable('alter', 'remove_row', index, [toIndex]) | Method | Remove the row at given index [optionally to another index] handsontable('alter', 'remove_col', index, [toIndex]) | Method | Remove the column at given index [optionally to another index] - handsontable('getCell', row, col) | Method | Return <td> element for given row, col + handsontable('getCell', row, col) | Method | Return <td> element for given `row,col` + handsontable('selectCell', r, c, [r2, c2, scrollToSelection=true]) | Method | Select cell `r,c` or range finishing at `r2,c2`. By default, viewport will be scrolled to selection + handsontable('deselectCell') | Method | Deselect current selection ## Options @@ -68,6 +70,8 @@ The table below presents configuration options that are interpreted by `handsont ------------------|-------------------|-------------|------------- `rows` | Number | 5 | Initial number of rows `cols` | Number | 5 | Initial number of columns + `rowHeaders` | Boolean/Array | false | Defines if the row headers (1, 2, 3, ...) should be displayed. You can just set it to `true` or specify custom a array `["First", "Second", "Third", ...]` + `colHeaders` | Boolean/Array | false | Defines if the column headers (A, B, C, ...) should be displayed. You can just set it to `true` or specify custom a array `["First Name", "Last Name", "Address", ...]` `minWidth` | Number | 0 | Handsontable will add as many columns as needed to meet the given width in pixels `minHeight` | Number | 0 | Handsontable will add as many rows as needed to meet the given height in pixels `minSpareCols` | Number | 0 | When set to 1 (or more), Handsontable will add a new column at the end of grid if there are no more empty columns @@ -102,14 +106,6 @@ legend: [ }, title: 'Heading', //make some tooltip readOnly: true //make it read-only - }, - { - match: function (row, col, data) { - return (row > 0 && data()[0][col].indexOf('color') > 0); //if first cell in this column contains word "color" - }, - style: { - fontStyle: 'italic' //make cells text in this column written in italic - } } ``` diff --git a/demo/js/demo.js b/demo/js/demo.js index 49351fe93a6..1e951641c42 100644 --- a/demo/js/demo.js +++ b/demo/js/demo.js @@ -30,8 +30,10 @@ $(function () { rows: 5, cols: 5, - fillHandle: false, - //fillHandle can be turned off + rowHeaders: true, + colHeaders: true, + + fillHandle: false, //fillHandle can be turned off contextMenu: ["row_above", "row_below", "remove_row"], //contextMenu will only allow inserting and removing rows @@ -45,8 +47,7 @@ $(function () { color: 'green', //make the text green and bold fontWeight: 'bold' }, - title: 'Heading', //make some tooltip - readOnly: true //make it read-only + title: 'Heading' //make some tooltip }, { match: function (row, col, data) { @@ -75,25 +76,27 @@ $(function () { */ $("#example3grid").handsontable({ rows: 7, - cols: 5, + cols: 4, + rowHeaders: false, //turn off 1, 2, 3, ... + colHeaders: ["Car", "Year", "Chassis color", "Bumper color"], legend: [ { match: function (row, col, data) { - return (row === 0); //if it is first row + if (col == 0 || col == 2 || col == 3) { + return true; + } + return false; }, style: { - color: '#666', //make the text gray and bold - fontWeight: 'bold' + fontStyle: 'italic' //make the text italic }, - title: 'Heading', //make some tooltip - readOnly: true //make it read-only + title: "Type to show the list of options" } ], autoComplete: [ { match: function (row, col, data) { - if (data()[0][col].indexOf("color") > -1) { - //if column name contains word "color" + if (col == 2 || col == 3) { return true; } return false; @@ -121,7 +124,6 @@ $(function () { }); var data = [ - ["Car", "Year", "Chassis color", "Bumper color"], ["Nissan", 2009, "black", "black"], ["Nissan", 2006, "blue", "blue"], ["Chrysler", 2004, "yellow", "black"], @@ -137,6 +139,8 @@ $(function () { $("#example4grid").handsontable({ rows: 40, cols: 40, + rowHeaders: true, + colHeaders: true, minSpareCols: 1, //always keep at least 1 spare row at the right minSpareRows: 1 //always keep at least 1 spare row at the bottom }); @@ -157,6 +161,8 @@ $(function () { $("#example5grid").handsontable({ rows: 8, cols: 8, + rowHeaders: true, + colHeaders: true, minSpareCols: 1, minSpareRows: 1, fillHandle: true //possible values: true, false, "horizontal", "vertical" @@ -179,6 +185,8 @@ $(function () { $("#example6grid").handsontable({ rows: 8, cols: 8, + rowHeaders: true, + colHeaders: true, minSpareCols: 1, minSpareRows: 1, contextMenu: true, @@ -223,6 +231,8 @@ $(function () { $("#example7grid").handsontable({ rows: 5, cols: 5, + rowHeaders: true, + colHeaders: true, minSpareCols: 1, minSpareRows: 1, contextMenu: true @@ -236,8 +246,6 @@ $(function () { ]; $("#example7grid").handsontable("loadData", data); - - } loadExamples(); @@ -252,5 +260,4 @@ $(function () { $this.find('a[href~=#' + $this.attr('id').replace('container', '') + ']').addClass('active'); }); examplesList.remove(); -}) -; \ No newline at end of file +}); \ No newline at end of file diff --git a/index.html b/index.html index 32a4f6c6e66..f57f93e25dd 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + Handsontable - jQuery grid editor. Excel-like grid editing with HTML & JavaScript @@ -40,7 +40,8 @@

Handsontable

- Handsontable is a minimalistic (60 KB unminified) approach to Excel-like table editor in HTML & jQuery + Handsontable is a minimalistic (70 KB unminified) approach to Excel-like table editor in HTML & jQuery. + Now with column and row headers!

Demos

@@ -117,9 +118,12 @@

Autoexpanding

Legend

-

The legend feature, which makes the first row uneditable and have an +

The legend feature, which makes the first row have an green font.

+

In the below example, the column (A, B, C) and row (1, 2, 3) headers are on. +

+

Code:

@@ -131,8 +135,10 @@

Legend

rows: 5, cols: 5, - fillHandle: false, - //fillHandle can be turned off + rowHeaders: true, + colHeaders: true, + + fillHandle: false, //fillHandle can be turned off contextMenu: ["row_above", "row_below", "remove_row"], //contextMenu will only allow inserting and removing rows @@ -146,13 +152,12 @@

Legend

color: 'green', //make the text green and bold fontWeight: 'bold' }, - title: 'Heading', //make some tooltip - readOnly: true //make it read-only + title: 'Heading' //make some tooltip }, { match: function (row, col, data) { //if first row in this column contains word "Nissan" - return (row > 0 && data()[0][col].indexOf('Nissan') > -1); + return (row > 0 && data()[0][col].indexOf('Nissan') > -1); }, style: { fontStyle: 'italic' //make cells text in this column written in italic @@ -194,35 +199,37 @@

Autocomplete

<script> $("#example3grid").handsontable({ rows: 7, - cols: 5, + cols: 4, + rowHeaders: false, //turn off 1, 2, 3, ... + colHeaders: ["Car", "Year", "Chassis color", "Bumper color"], legend: [ { match: function (row, col, data) { - return (row === 0); //if it is first row + if (col == 0 || col == 2 || col == 3) { + return true; + } + return false; }, style: { - color: '#666', //make the text gray and bold - fontWeight: 'bold' + fontStyle: 'italic' //make the text italic }, - title: 'Heading', //make some tooltip - readOnly: true //make it read-only + title: "Type to show the list of options" } ], autoComplete: [ { match: function (row, col, data) { - if (data()[0][col].indexOf("color") > -1) { - //if column name contains word "color" + if (col == 2 || col == 3) { return true; } return false; }, highlighter: function (item) { - var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); + var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); var label = item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { return '<strong>' + match + '</strong>'; }); - return '<span style="margin-right: 10px; background-color: ' + item + '">   </span>' + label; + return '<span style="margin-right: 10px; background-color: ' + item + '">&nbsp;&nbsp;&nbsp;</span>' + label; }, source: function () { return ["yellow", "red", "orange", "green", "blue", "gray", "black", "white"] @@ -240,7 +247,6 @@

Autocomplete

}); var data = [ - ["Car", "Year", "Chassis color", "Bumper color"], ["Nissan", 2009, "black", "black"], ["Nissan", 2006, "blue", "blue"], ["Chrysler", 2004, "yellow", "black"], @@ -271,6 +277,8 @@

Scroll

$("#example4grid").handsontable({ rows: 40, cols: 40, + rowHeaders: true, + colHeaders: true, minSpareCols: 1, //always keep at least 1 spare row at the right minSpareRows: 1 //always keep at least 1 spare row at the bottom }); @@ -308,6 +316,8 @@

Drag-down

$("#example5grid").handsontable({ rows: 8, cols: 8, + rowHeaders: true, + colHeaders: true, minSpareCols: 1, minSpareRows: 1, fillHandle: true //possible values: true, false, "horizontal", "vertical" @@ -343,6 +353,8 @@

Context menu

$("#example7grid").handsontable({ rows: 5, cols: 5, + rowHeaders: true, + colHeaders: true, minSpareCols: 1, minSpareRows: 1, contextMenu: true @@ -392,11 +404,13 @@

Save

$("#example6grid").handsontable({ rows: 8, cols: 8, + rowHeaders: true, + colHeaders: true, minSpareCols: 1, minSpareRows: 1, contextMenu: true, onBeforeChange: function (data) { - for (var i = 0, ilen = data.length; i < ilen; i++) { + for (var i = 0, ilen = data.length; i < ilen; i++) { if (data[i][3] === "foo") { //gently don't accept the word "foo" data[i][3] = false; diff --git a/jquery.handsontable.css b/jquery.handsontable.css index 939f9272bb5..1f62a67402f 100644 --- a/jquery.handsontable.css +++ b/jquery.handsontable.css @@ -9,7 +9,7 @@ } .dataTable table { - border-collapse: collapse; + border-collapse: separate; position: relative; -webkit-user-select: none; -khtml-user-select: none; @@ -21,6 +21,7 @@ border-left: 1px solid #CCC; } +.dataTable th, .dataTable td { border-right: 1px solid #CCC; border-bottom: 1px solid #CCC; @@ -30,6 +31,23 @@ padding: 0 4px 0 4px; } +.dataTable th { + background-color: #EEE; + font-size: 12px; + color: #222; + text-align: center; + font-weight: normal; + white-space: nowrap; +} + +.dataTable thead th { + padding: 4px; +} + +.dataTable th.active { + background-color: #CCC; +} + /* border background */ .dataTable .htBorderBg { position: absolute; diff --git a/jquery.handsontable.js b/jquery.handsontable.js index 09462bdff83..6bd30762d65 100644 --- a/jquery.handsontable.js +++ b/jquery.handsontable.js @@ -22,14 +22,13 @@ selStart: null, selEnd: null, editProxy: false, - table: null, isPopulated: null, - rowCount: 0, - colCount: 0, scrollable: null, hasLegend: null, lastAutoComplete: null, - undoRedo: settings.undo ? new handsontable.UndoRedo(this) : null + undoRedo: settings.undo ? new handsontable.UndoRedo(this) : null, + rowHeaderCount: 0, + colHeaderCount: 0 }; var lastChange = ''; @@ -57,10 +56,10 @@ */ createRow: function (coords) { var row = []; - for (var c = 0; c < priv.colCount; c++) { + for (var c = 0; c < self.colCount; c++) { row.push(''); } - if (!coords || coords.row >= priv.rowCount) { + if (!coords || coords.row >= self.rowCount) { datamap.data.push(row); } else { @@ -74,13 +73,13 @@ */ createCol: function (coords) { var r = 0; - if (!coords || coords.col >= priv.colCount) { - for (; r < priv.rowCount; r++) { + if (!coords || coords.col >= self.colCount) { + for (; r < self.rowCount; r++) { datamap.data[r].push(''); } } else { - for (; r < priv.rowCount; r++) { + for (; r < self.rowCount; r++) { datamap.data[r].splice(coords.col, 0, ''); } } @@ -92,7 +91,7 @@ * @param {Object} [toCoords] Required if coords is defined. Coords of the cell until which all rows will be removed */ removeRow: function (coords, toCoords) { - if (!coords || coords.row === priv.rowCount - 1) { + if (!coords || coords.row === self.rowCount - 1) { datamap.data.pop(); } else { @@ -107,14 +106,14 @@ */ removeCol: function (coords, toCoords) { var r = 0; - if (!coords || coords.col === priv.colCount - 1) { - for (; r < priv.rowCount; r++) { + if (!coords || coords.col === self.colCount - 1) { + for (; r < self.rowCount; r++) { datamap.data[r].pop(); } } else { var howMany = toCoords.col - coords.col + 1; - for (; r < priv.rowCount; r++) { + for (; r < self.rowCount; r++) { datamap.data[r].splice(coords.col, howMany); } } @@ -126,7 +125,7 @@ * @param {Number} col */ get: function (row, col) { - return datamap.data[row] ? datamap.data[row][col] : void 0; //void 0 produces undefined + return datamap.data[row] ? datamap.data[row][col] : null; }, /** @@ -143,8 +142,8 @@ * Clears the data array */ clear: function () { - for (var r = 0; r < priv.rowCount; r++) { - for (var c = 0; c < priv.colCount; c++) { + for (var r = 0; r < self.rowCount; r++) { + for (var c = 0; c < self.colCount; c++) { datamap.data[r][c] = ''; } } @@ -207,30 +206,38 @@ * @param {Object} [toCoords] Required only for actions "remove_row" and "remove_col" */ alter: function (action, coords, toCoords) { - var oldData, newData, changes, r, rlen, c, clen; + var oldData, newData, changes, r, rlen, c, clen, result; oldData = $.extend(true, [], datamap.getAll()); switch (action) { case "insert_row": datamap.createRow(coords); grid.createRow(coords); + priv.rowHeader && priv.rowHeader.refresh(); break; case "insert_col": datamap.createCol(coords); grid.createCol(coords); + priv.columnHeader && priv.columnHeader.refresh(); break; case "remove_row": datamap.removeRow(coords, toCoords); grid.removeRow(coords, toCoords); - grid.keepEmptyRows(); + result = grid.keepEmptyRows(); + if (!result) { + priv.rowHeader && priv.rowHeader.refresh(); + } break; case "remove_col": datamap.removeCol(coords, toCoords); grid.removeCol(coords, toCoords); - grid.keepEmptyRows(); + result = grid.keepEmptyRows(); + if (!result) { + priv.columnHeader && priv.columnHeader.refresh(); + } break; } @@ -251,20 +258,26 @@ createRow: function (coords) { var tr, c, r; tr = document.createElement('tr'); - for (c = 0; c < priv.colCount; c++) { + + if (priv.rowHeader) { + var th = document.createElement('th'); + tr.appendChild(th); + } + + for (c = 0; c < self.colCount; c++) { tr.appendChild(document.createElement('td')); } - if (!coords || coords.row >= priv.rowCount) { + if (!coords || coords.row >= self.rowCount) { priv.tableBody.appendChild(tr); - r = priv.rowCount; + r = self.rowCount; } else { var oldTr = grid.getCellAtCoords(coords).parentNode; priv.tableBody.insertBefore(tr, oldTr); r = coords.row; } - priv.rowCount++; - for (c = 0; c < priv.colCount; c++) { + self.rowCount++; + for (c = 0; c < self.colCount; c++) { grid.updateLegend({row: r, col: c}); } }, @@ -275,20 +288,31 @@ */ createCol: function (coords) { var trs = priv.tableBody.childNodes, r, c; - if (!coords || coords.col >= priv.colCount) { - for (r = 0; r < priv.rowCount; r++) { + + if (priv.columnHeader) { + var tr = self.table.find('thead tr')[0]; + if (!coords || coords.col >= self.colCount) { + tr.appendChild(document.createElement('th')); + } + else { + tr.insertBefore(document.createElement('th'), tr.childNodes[coords.col + 1]); + } + } + + if (!coords || coords.col >= self.colCount) { + for (r = 0; r < self.rowCount; r++) { trs[r].appendChild(document.createElement('td')); } - c = priv.colCount; + c = self.colCount; } else { - for (r = 0; r < priv.rowCount; r++) { + for (r = 0; r < self.rowCount; r++) { trs[r].insertBefore(document.createElement('td'), grid.getCellAtCoords({row: r, col: coords.col})); } c = coords.col; } - priv.colCount++; - for (r = 0; r < priv.rowCount; r++) { + self.colCount++; + for (r = 0; r < self.rowCount; r++) { grid.updateLegend({row: r, col: c}); } }, @@ -299,14 +323,14 @@ * @param {Object} [toCoords] Required if coords is defined. Coords of the cell until which all rows will be removed */ removeRow: function (coords, toCoords) { - if (!coords || coords.row === priv.rowCount - 1) { - $(priv.tableBody.childNodes[priv.rowCount - 1]).remove(); - priv.rowCount--; + if (!coords || coords.row === self.rowCount - 1) { + $(priv.tableBody.childNodes[self.rowCount - 1]).remove(); + self.rowCount--; } else { for (var i = toCoords.row; i >= coords.row; i--) { $(priv.tableBody.childNodes[i]).remove(); - priv.rowCount--; + self.rowCount--; } } }, @@ -319,19 +343,19 @@ removeCol: function (coords, toCoords) { var trs = priv.tableBody.childNodes; var r = 0; - if (!coords || coords.col === priv.colCount - 1) { - for (; r < priv.rowCount; r++) { - $(trs[r].childNodes[priv.colCount - 1]).remove(); + if (!coords || coords.col === self.colCount - 1) { + for (; r < self.rowCount; r++) { + $(trs[r].childNodes[self.colCount + priv.rowHeaderCount - 1]).remove(); } - priv.colCount--; + self.colCount--; } else { - for (; r < priv.rowCount; r++) { + for (; r < self.rowCount; r++) { for (var i = toCoords.col; i >= coords.col; i--) { - $(trs[r].childNodes[i]).remove(); + $(trs[r].childNodes[i + priv.rowHeaderCount]).remove(); } } - priv.colCount -= toCoords.col - coords.col + 1; + self.colCount -= toCoords.col - coords.col + 1; } }, @@ -357,8 +381,8 @@ } //should I add empty rows to meet minSpareRows? - if (priv.rowCount < priv.settings.rows || emptyRows < priv.settings.minSpareRows) { - for (; priv.rowCount < priv.settings.rows || emptyRows < priv.settings.minSpareRows; emptyRows++) { + if (self.rowCount < priv.settings.rows || emptyRows < priv.settings.minSpareRows) { + for (; self.rowCount < priv.settings.rows || emptyRows < priv.settings.minSpareRows; emptyRows++) { datamap.createRow(); grid.createRow(); recreateRows = true; @@ -393,8 +417,8 @@ } //should I add empty cols to meet minSpareCols? - if (priv.colCount < priv.settings.cols || emptyCols < priv.settings.minSpareCols) { - for (; priv.colCount < priv.settings.cols || emptyCols < priv.settings.minSpareCols; emptyCols++) { + if (self.colCount < priv.settings.cols || emptyCols < priv.settings.minSpareCols) { + for (; self.colCount < priv.settings.cols || emptyCols < priv.settings.minSpareCols; emptyCols++) { datamap.createCol(); grid.createCol(); recreateCols = true; @@ -414,7 +438,7 @@ } if (!recreateRows) { - for (; ((priv.settings.rows && priv.rowCount > priv.settings.rows) && (priv.settings.minSpareRows && emptyRows > priv.settings.minSpareRows) && (!priv.settings.minHeight || $tbody.height() - $tbody.find('tr:last').height() - 4 > priv.settings.minHeight)); emptyRows--) { + for (; ((priv.settings.rows && self.rowCount > priv.settings.rows) && (priv.settings.minSpareRows && emptyRows > priv.settings.minSpareRows) && (!priv.settings.minHeight || $tbody.height() - $tbody.find('tr:last').height() - 4 > priv.settings.minHeight)); emptyRows--) { grid.removeRow(); datamap.removeRow(); recreateRows = true; @@ -423,13 +447,13 @@ if (recreateRows && priv.selStart) { //if selection is outside, move selection to last row - if (priv.selStart.row > priv.rowCount - 1) { - priv.selStart.row = priv.rowCount - 1; + if (priv.selStart.row > self.rowCount - 1) { + priv.selStart.row = self.rowCount - 1; if (priv.selEnd.row > priv.selStart.row) { priv.selEnd.row = priv.selStart.row; } - } else if (priv.selEnd.row > priv.rowCount - 1) { - priv.selEnd.row = priv.rowCount - 1; + } else if (priv.selEnd.row > self.rowCount - 1) { + priv.selEnd.row = self.rowCount - 1; if (priv.selStart.row > priv.selEnd.row) { priv.selStart.row = priv.selEnd.row; } @@ -437,7 +461,7 @@ } if (!recreateCols) { - for (; ((priv.settings.cols && priv.colCount > priv.settings.cols) && (priv.settings.minSpareCols && emptyCols > priv.settings.minSpareCols) && (!priv.settings.minWidth || $tbody.width() - $tbody.find('tr:last').find('td:last').width() - 4 > priv.settings.minWidth)); emptyCols--) { + for (; ((priv.settings.cols && self.colCount > priv.settings.cols) && (priv.settings.minSpareCols && emptyCols > priv.settings.minSpareCols) && (!priv.settings.minWidth || $tbody.width() - $tbody.find('tr:last').find('td:last').width() - 4 > priv.settings.minWidth)); emptyCols--) { datamap.removeCol(); grid.removeCol(); recreateCols = true; @@ -446,13 +470,13 @@ if (recreateCols && priv.selStart) { //if selection is outside, move selection to last row - if (priv.selStart.col > priv.colCount - 1) { - priv.selStart.col = priv.colCount - 1; + if (priv.selStart.col > self.colCount - 1) { + priv.selStart.col = self.colCount - 1; if (priv.selEnd.col > priv.selStart.col) { priv.selEnd.col = priv.selStart.col; } - } else if (priv.selEnd.col > priv.colCount - 1) { - priv.selEnd.col = priv.colCount - 1; + } else if (priv.selEnd.col > self.colCount - 1) { + priv.selEnd.col = self.colCount - 1; if (priv.selStart.col > priv.selEnd.col) { priv.selStart.col = priv.selEnd.col; } @@ -461,6 +485,8 @@ if (recreateRows || recreateCols) { selection.refreshBorders(); + priv.rowHeader && priv.rowHeader.refresh(); + priv.columnHeader && priv.columnHeader.refresh(); } return (recreateRows || recreateCols); @@ -582,8 +608,8 @@ */ getCellCoords: function (td) { return { - row: td.parentNode.rowIndex, - col: td.cellIndex + row: td.parentNode.rowIndex - priv.colHeaderCount, + col: td.cellIndex - priv.rowHeaderCount }; }, @@ -596,7 +622,7 @@ } var tr = priv.tableBody.childNodes[coords.row]; if (tr) { - return tr.childNodes[coords.col]; + return tr.childNodes[coords.col + priv.rowHeaderCount]; } else { return null; @@ -655,12 +681,12 @@ getAllCells: function () { var tds = [], trs, r, rlen, c, clen; trs = priv.tableBody.childNodes; - rlen = priv.rowCount; + rlen = self.rowCount; if (rlen > 0) { - clen = priv.colCount; + clen = self.colCount; for (r = 0; r < rlen; r++) { for (c = 0; c < clen; c++) { - tds.push(trs[r].childNodes[c]); + tds.push(trs[r].childNodes[c + priv.rowHeaderCount]); } } } @@ -681,16 +707,19 @@ /** * Ends selection range on given td object - * @param td element + * @param {Element} td + * @param {Boolean} [scrollToCell=true] If true, viewport will be scrolled to range end */ - setRangeEnd: function (td) { + setRangeEnd: function (td, scrollToCell) { var coords = grid.getCellCoords(td); selection.end(coords); if (!priv.settings.multiSelect) { priv.selStart = coords; } selection.refreshBorders(); - highlight.scrollViewport(td); + if (scrollToCell !== false) { + highlight.scrollViewport(td); + } }, /** @@ -805,6 +834,7 @@ autofill.hideHandle(); } selection.end(false); + self.container.trigger('deselect.handsontable'); }, /** @@ -817,7 +847,7 @@ var tds = grid.getAllCells(); if (tds.length) { selection.setRangeStart(tds[0]); - selection.setRangeEnd(tds[tds.length - 1]); + selection.setRangeEnd(tds[tds.length - 1], false); } }, @@ -902,6 +932,9 @@ var scrollHeight = priv.scrollable.outerHeight() - 24; //24 = scrollbar var scrollOffset = priv.scrollable.offset(); + var rowHeaderWidth = priv.rowHeader ? $(priv.rowHeader.main[0].firstChild).outerWidth() : 2; + var colHeaderHeight = priv.columnHeader ? $(priv.columnHeader.main[0].firstChild).outerHeight() : 2; + var offsetTop = tdOffset.top; var offsetLeft = tdOffset.left; if (scrollOffset) { //if is not the window @@ -917,9 +950,9 @@ priv.scrollable.scrollLeft(offsetLeft + width - scrollWidth); }, 1); } - else if (scrollLeft > offsetLeft) { + else if (scrollLeft > offsetLeft - rowHeaderWidth) { setTimeout(function () { - priv.scrollable.scrollLeft(offsetLeft - 2); + priv.scrollable.scrollLeft(offsetLeft - rowHeaderWidth); }, 1); } @@ -928,9 +961,9 @@ priv.scrollable.scrollTop(offsetTop + height - scrollHeight); }, 1); } - else if (scrollTop > offsetTop) { + else if (scrollTop > offsetTop - colHeaderHeight) { setTimeout(function () { - priv.scrollable.scrollTop(offsetTop - 2); + priv.scrollable.scrollTop(offsetTop - colHeaderHeight); }, 1); } } @@ -966,7 +999,7 @@ if (select.TL.col > 0) { data = datamap.getAll(); - rows : for (r = select.BR.row + 1; r < priv.rowCount; r++) { + rows : for (r = select.BR.row + 1; r < self.rowCount; r++) { for (c = select.TL.col; c <= select.BR.col; c++) { if (data[r][c]) { break rows; @@ -1269,10 +1302,10 @@ case 35: /* end */ if (!priv.isCellEdited) { if (event.ctrlKey || event.metaKey) { - rangeModifier(grid.getCellAtCoords({row: priv.rowCount - 1, col: priv.selStart.col})); + rangeModifier(grid.getCellAtCoords({row: self.rowCount - 1, col: priv.selStart.col})); } else { - rangeModifier(grid.getCellAtCoords({row: priv.selStart.row, col: priv.colCount - 1})); + rangeModifier(grid.getCellAtCoords({row: priv.selStart.row, col: self.colCount - 1})); } } break; @@ -1282,7 +1315,7 @@ break; case 34: /* pg dn */ - rangeModifier(grid.getCellAtCoords({row: priv.rowCount - 1, col: priv.selStart.col})); + rangeModifier(grid.getCellAtCoords({row: self.rowCount - 1, col: priv.selStart.col})); break; default: @@ -1545,14 +1578,34 @@ priv.isMouseOverTable = false; } - priv.table = $('
'); - priv.tableBody = priv.table.find("tbody")[0]; - priv.table.on('mousedown', 'td', interaction.onMouseDown); - priv.table.on('mouseover', 'td', interaction.onMouseOver); - priv.table.on('dblclick', 'td', interaction.onDblClick); - container.append(priv.table); + self.table = $('
'); + priv.tableBody = self.table.find("tbody")[0]; + self.table.on('mousedown', 'td', interaction.onMouseDown); + self.table.on('mouseover', 'td', interaction.onMouseOver); + self.table.on('dblclick', 'td', interaction.onDblClick); + container.append(self.table); + + self.colCount = priv.settings.cols; + self.rowCount = 0; + + if (priv.settings.colHeaders) { + priv.colHeaderCount = 1; + priv.columnHeader = new handsontable.ColumnHeader(self, priv.settings.colHeaders, 1 * !!priv.settings.rowHeaders); + } + + if (priv.settings.rowHeaders) { + priv.rowHeaderCount = 1; + priv.rowHeader = new handsontable.RowHeader(self, priv.settings.rowHeaders, 1 * !!priv.settings.colHeaders); + } + + if (priv.colHeaderCount && priv.rowHeaderCount) { + priv.cornerHeader = $('
 
'); + priv.cornerHeader.on('click', function () { + selection.selectAll(); + }); + container.append(priv.cornerHeader); + } - priv.colCount = priv.settings.cols; grid.keepEmptyRows(); highlight.init(); @@ -1565,8 +1618,8 @@ } editproxy.init(); - priv.table.on('mouseenter', onMouseEnterTable); - priv.table.on('mouseleave', onMouseLeaveTable); + self.table.on('mouseenter', onMouseEnterTable); + self.table.on('mouseleave', onMouseLeaveTable); priv.editProxy.on('mouseenter', onMouseEnterTable); priv.editProxy.on('mouseleave', onMouseLeaveTable); if (priv.fillHandle) { @@ -1575,6 +1628,9 @@ } $(priv.selectionBorder.main).on('mouseenter', onMouseEnterTable).on('mouseleave', onMouseLeaveTable); $(priv.currentBorder.main).on('mouseenter', onMouseEnterTable).on('mouseleave', onMouseLeaveTable).on('dblclick', interaction.onDblClick); + priv.rowHeader && $(priv.rowHeader.main).on('mouseenter', onMouseEnterTable).on('mouseleave', onMouseLeaveTable); + priv.columnHeader && $(priv.columnHeader.main).on('mouseenter', onMouseEnterTable).on('mouseleave', onMouseLeaveTable); + priv.cornerHeader && $(priv.cornerHeader).on('mouseenter', onMouseEnterTable).on('mouseleave', onMouseLeaveTable); function onMouseUp() { priv.isMouseDown = false; @@ -1612,6 +1668,30 @@ if (priv.scrollable) { priv.scrollable.scrollTop(0); priv.scrollable.scrollLeft(0); + + var lastScrollTop = 0; + var curScrollTop; + var lastScrollLeft = 0; + var curScrollLeft; + + priv.scrollable.on('scroll', function () { + curScrollTop = priv.scrollable[0].scrollTop; + curScrollLeft = priv.scrollable[0].scrollLeft; + if (priv.columnHeader && curScrollTop !== lastScrollTop) { + priv.columnHeader.main[0].style.top = curScrollTop + 'px'; + if (priv.cornerHeader) { + priv.cornerHeader[0].style.top = curScrollTop + 'px'; + } + lastScrollTop = curScrollTop; + } + if (priv.rowHeader && curScrollLeft !== lastScrollLeft) { + priv.rowHeader.main[0].style.left = curScrollLeft + 'px'; + if (priv.cornerHeader) { + priv.cornerHeader[0].style.left = curScrollLeft + 'px'; + } + lastScrollLeft = curScrollLeft; + } + }); } else { priv.scrollable = $(window); @@ -1657,15 +1737,26 @@ } }; - var isReadOnly = function (key) { - var coords = grid.getCornerCoords([priv.selStart, priv.selEnd]); + var isDisabled = function (key) { + if (priv.rowHeader && priv.rowHeader.lastActive && (key === "remove_col" || key === "col_left" || key === "col_right")) { + return true; + } - if (((key === "row_above" || key === "remove_row") && coords.TL.row === 0) || ((key === "col_left" || key === "remove_col") && coords.TL.col === 0)) { - if ($(grid.getCellAtCoords(coords.TL)).data("readOnly")) { - return true; + if (priv.columnHeader && priv.columnHeader.lastActive && (key === "remove_row" || key === "row_above" || key === "row_below")) { + return true; + } + + if (priv.selStart) { + var coords = grid.getCornerCoords([priv.selStart, priv.selEnd]); + if (((key === "row_above" || key === "remove_row") && coords.TL.row === 0) || ((key === "col_left" || key === "remove_col") && coords.TL.col === 0)) { + if ($(grid.getCellAtCoords(coords.TL)).data("readOnly")) { + return true; + } } + return false; } - return false; + + return true; }; var allItems = { @@ -1676,14 +1767,14 @@ return priv.undoRedo ? !priv.undoRedo.isRedoAvailable() : true }}, "sep1": "---------", - "row_above": {name: "Insert row above", disabled: isReadOnly}, - "row_below": {name: "Insert row below"}, + "row_above": {name: "Insert row above", disabled: isDisabled}, + "row_below": {name: "Insert row below", disabled: isDisabled}, "sep2": "---------", - "col_left": {name: "Insert column on the left", disabled: isReadOnly}, - "col_right": {name: "Insert column on the right"}, + "col_left": {name: "Insert column on the left", disabled: isDisabled}, + "col_right": {name: "Insert column on the right", disabled: isDisabled}, "sep3": "---------", - "remove_row": {name: "Remove row", disabled: isReadOnly}, - "remove_col": {name: "Remove column", disabled: isReadOnly} + "remove_row": {name: "Remove row", disabled: isDisabled}, + "remove_col": {name: "Remove column", disabled: isDisabled} }; if (priv.settings.contextMenu === true) { //contextMenu is true, not an array @@ -1719,16 +1810,27 @@ * @param {Boolean} [allowHtml] */ this.setDataAtCell = function (row, col, value, allowHtml) { + var refresh; if (priv.settings.minSpareRows) { - while (row > priv.rowCount - 1) { + refresh = false; + while (row > self.rowCount - 1) { datamap.createRow(); grid.createRow(); + refresh = true; + } + if (refresh) { + priv.rowHeader && priv.rowHeader.refresh(); } } if (priv.settings.minSpareCols) { - while (col > priv.colCount - 1) { + refresh = false; + while (col > self.colCount - 1) { datamap.createCol(); grid.createCol(); + refresh = true; + } + if (refresh) { + priv.columnHeader && priv.columnHeader.refresh(); } } var td = grid.getCellAtCoords({row: row, col: col}); @@ -1828,7 +1930,6 @@ } }; - /** * Returns element corresponding to params row, col * @param {Number} row @@ -1840,6 +1941,33 @@ return grid.getCellAtCoords({row: row, col: col}); }; + /** + * Selects cell on grid. Optionally selects range to another cell + * @param {Number} row + * @param {Number} col + * @param {Number} [endRow] + * @param {Number} [endCol] + * @param {Boolean} [scrollToCell=true] If true, viewport will be scrolled to the selection + * @public + */ + this.selectCell = function (row, col, endRow, endCol, scrollToCell) { + selection.start({row: row, col: col}); + if (typeof endRow === "undefined") { + selection.setRangeEnd(self.getCell(row, col), scrollToCell); + } + else { + selection.setRangeEnd(self.getCell(endRow, endCol), scrollToCell); + } + }; + + /** + * Deselects current sell selection on grid + * @public + */ + this.deselectCell = function () { + selection.deselect(); + }; + /** * Create DOM elements for selection border lines (top, right, bottom, left) and optionally background * @constructor @@ -2136,4 +2264,190 @@ handsontable.UndoRedo.prototype.add = function (changes) { this.rev++; this.data.splice(this.rev); //if we are in point abcdef(g)hijk in history, remove everything after (g) this.data.push(changes); -}; \ No newline at end of file +}; + + +/** + * Handsontable ColumnHeader extension + * @param {Object} instance + * @param {Array|Boolean} [labels] + * @param {Number} [offset] + */ +handsontable.ColumnHeader = function (instance, labels, offset) { + var that = this; + this.instance = instance; + this.labels = labels || []; + this.offset = offset || 0; + this.create(); + this.main = $('
'); + this.main.on('mousedown', 'th', function () { + that.instance.deselectCell(); + this.className = 'active'; + that.lastActive = this; + var index = $(this).index(); + that.instance.selectCell(0, index - that.offset, that.instance.rowCount - 1, index - that.offset, false); + }); + this.instance.container.on('deselect.handsontable', function () { + that.deselect(); + }); + this.instance.container.on('datachange.handsontable', function (event, changes) { + setTimeout(function () { + that.dimensions(changes); + }, 10); + }); + this.instance.container.append(this.main); +}; + +/** + * + */ +handsontable.ColumnHeader.prototype.create = function () { + var tr, c; + tr = document.createElement('tr'); + for (c = 0; c < this.instance.colCount + this.offset; c++) { + var th = document.createElement('th'); + tr.appendChild(th); + } + this.instance.table.find('thead').append(tr); +}; + +/** + * + */ +handsontable.ColumnHeader.prototype.columnLabel = function (columnNumber) { + if (this.labels[columnNumber]) { + return this.labels[columnNumber]; + } + var dividend = columnNumber + 1; + var columnLabel = ''; + var modulo; + while (dividend > 0) { + modulo = (dividend - 1) % 26; + columnLabel = String.fromCharCode(65 + modulo) + columnLabel; + dividend = parseInt((dividend - modulo) / 26); + } + return columnLabel; +}; + +/** + * + */ +handsontable.ColumnHeader.prototype.refresh = function () { + var that = this; + var tr = this.main.find('tr'); + tr.empty(); + this.instance.table.find("thead th").each(function (index) { + this.innerHTML = that.columnLabel(index - that.offset); + var $this = $(this); + var th = $this.clone(); + th[0].style.minWidth = $this.width() + 'px'; + tr.append(th); + }); +}; + +/** + * + */ +handsontable.ColumnHeader.prototype.dimensions = function (changes) { + for (var i = 0, ilen = changes.length; i < ilen; i++) { + var $th = $(this.instance.getCell(changes[i][0], changes[i][1])); + if ($th.length) { + var width = $th.width(); + this.main.find('th').get(changes[i][1] + this.offset).style.minWidth = width + 'px'; + } + } +}; + +/** + * + */ +handsontable.ColumnHeader.prototype.deselect = function () { + if (this.lastActive) { + this.lastActive.className = ''; + this.lastActive = null; + } +}; + + +/** + * Handsontable RowHeader extension + * @param {Object} instance + * @param {Array|Boolean} [labels] + * @param {Number} [offset] + */ +handsontable.RowHeader = function (instance, labels, offset) { + var that = this; + this.instance = instance; + this.labels = labels || []; + this.offset = offset || 0; + var offsetStr = ''; + if (this.offset) { + offsetStr = ' '; + } + this.main = $('
' + offsetStr + '
'); + this.main.on('mousedown', 'th', function () { + that.instance.deselectCell(); + this.className = 'active'; + that.lastActive = this; + that.instance.selectCell(this.parentNode.rowIndex - that.offset, 0, this.parentNode.rowIndex - that.offset, that.instance.colCount - 1, false); + }); + this.instance.container.on('deselect.handsontable', function () { + that.deselect(); + }); + this.instance.container.on('datachange.handsontable', function (event, changes) { + setTimeout(function () { + that.dimensions(changes); + }, 10); + }); + this.instance.container.append(this.main); +}; + +/** + * + */ +handsontable.RowHeader.prototype.columnLabel = function (columnNumber) { + if (this.labels[columnNumber]) { + return this.labels[columnNumber]; + } + return columnNumber + 1; +}; + +/** + * + */ +handsontable.RowHeader.prototype.refresh = function () { + var that = this; + this.main.find('tbody').empty(); + this.instance.table.find('tbody tr').each(function (index) { + var tr = $(""); + $(this).find('th').each(function () { + this.innerHTML = that.columnLabel(index); + var $this = $(this); + var height = $this.height(); + var th = $this.clone(); + th[0].style.height = height + 'px'; + th[0].style.lineHeight = height + 'px'; + tr.append(th); + }); + that.main.find('tbody').append(tr); + }); +}; + +/** + * + */ +handsontable.RowHeader.prototype.dimensions = function (changes) { + for (var i = 0, ilen = changes.length; i < ilen; i++) { + var $th = $(this.instance.getCell(changes[i][0], changes[i][1])); + if ($th.length) { + var height = $th.height(); + this.main.find('th').get(changes[i][0] + this.offset).style.height = height + 'px'; + this.main.find('th').get(changes[i][0] + this.offset).style.lineHeight = height + 'px'; + } + } +}; + +/** + * + */ +handsontable.RowHeader.prototype.deselect = handsontable.ColumnHeader.prototype.deselect; \ No newline at end of file