[ Index ] |
PHP Cross Reference of vtigercrm-6.1.0 |
[Summary view] [Print] [Text view]
1 /** 2 * Handsontable 0.7.0-beta 3 * Handsontable is a simple jQuery plugin for editable tables with basic copy-paste compatibility with Excel and Google Docs 4 * 5 * Copyright 2012, Marcin Warpechowski 6 * Licensed under the MIT license. 7 * http://handsontable.com/ 8 */ 9 /*jslint white: true, browser: true, plusplus: true, indent: 4, maxerr: 50 */ 10 11 var Handsontable = { //class namespace 12 extension: {}, //extenstion namespace 13 helper: {} //helper namespace 14 }; 15 16 (function ($, window, Handsontable) { 17 "use strict"; 18 /** 19 * Handsontable constructor 20 * @param rootElement The jQuery element in which Handsontable DOM will be inserted 21 * @param settings 22 * @constructor 23 */ 24 Handsontable.Core = function (rootElement, settings) { 25 this.rootElement = rootElement; 26 27 var priv, datamap, grid, selection, editproxy, highlight, autofill, self = this; 28 29 priv = { 30 settings: {}, 31 selStart: null, 32 selEnd: null, 33 editProxy: false, 34 isPopulated: null, 35 scrollable: null, 36 undoRedo: null, 37 extensions: {}, 38 colToProp: [], 39 propToCol: {}, 40 dataSchema: null, 41 dataType: 'array' 42 }; 43 44 var hasMinWidthProblem = ($.browser.msie && (parseInt($.browser.version, 10) <= 7)); 45 /** 46 * Used to get over IE7 not respecting CSS min-width (and also not showing border around empty cells) 47 * @param {Element} td 48 */ 49 this.minWidthFix = function (td) { 50 if (hasMinWidthProblem) { 51 if (td.className) { 52 td.innerHTML = '<div class="minWidthFix ' + td.className + '">' + td.innerHTML + '</div>'; 53 } 54 else { 55 td.innerHTML = '<div class="minWidthFix">' + td.innerHTML + '</div>'; 56 } 57 } 58 }; 59 60 var hasPositionProblem = ($.browser.msie && (parseInt($.browser.version, 10) <= 7)); 61 /** 62 * Used to get over IE7 returning negative position in demo/buttons.html 63 * @param {Object} position 64 */ 65 this.positionFix = function (position) { 66 if (hasPositionProblem) { 67 if (position.top < 0) { 68 position.top = 0; 69 } 70 if (position.left < 0) { 71 position.left = 0; 72 } 73 } 74 }; 75 76 datamap = { 77 recursiveDuckSchema: function (obj) { 78 var schema; 79 if ($.isPlainObject(obj)) { 80 schema = {}; 81 for (var i in obj) { 82 if (obj.hasOwnProperty(i)) { 83 if ($.isPlainObject(obj[i])) { 84 schema[i] = datamap.recursiveDuckSchema(obj[i]); 85 } 86 else { 87 schema[i] = null; 88 } 89 } 90 } 91 } 92 else { 93 schema = []; 94 } 95 return schema; 96 }, 97 98 recursiveDuckColumns: function (schema, lastCol, parent) { 99 var prop, i; 100 if (typeof lastCol === 'undefined') { 101 lastCol = 0; 102 parent = ''; 103 } 104 if ($.isPlainObject(schema)) { 105 for (i in schema) { 106 if (schema.hasOwnProperty(i)) { 107 if (schema[i] === null) { 108 prop = parent + i; 109 priv.colToProp.push(prop); 110 priv.propToCol[prop] = lastCol; 111 lastCol++; 112 } 113 else { 114 lastCol = datamap.recursiveDuckColumns(schema[i], lastCol, i + '.'); 115 } 116 } 117 } 118 } 119 return lastCol; 120 }, 121 122 createMap: function () { 123 if (typeof datamap.getSchema() === "undefined") { 124 throw new Error("trying to create `columns` definition but you didnt' provide `schema` nor `data`"); 125 } 126 var i, ilen, schema = datamap.getSchema(); 127 priv.colToProp = []; 128 priv.propToCol = {}; 129 if (priv.settings.columns) { 130 for (i = 0, ilen = priv.settings.columns.length; i < ilen; i++) { 131 priv.colToProp[i] = priv.settings.columns[i].data; 132 priv.propToCol[priv.settings.columns[i].data] = i; 133 } 134 } 135 else { 136 datamap.recursiveDuckColumns(schema); 137 } 138 }, 139 140 colToProp: function (col) { 141 if (typeof priv.colToProp[col] !== 'undefined') { 142 return priv.colToProp[col]; 143 } 144 else { 145 return col; 146 } 147 }, 148 149 propToCol: function (prop) { 150 if (typeof priv.propToCol[prop] !== 'undefined') { 151 return priv.propToCol[prop]; 152 } 153 else { 154 return prop; 155 } 156 157 }, 158 159 getSchema: function () { 160 return priv.settings.dataSchema || priv.duckDataSchema; 161 }, 162 163 /** 164 * Creates row at the bottom of the data array 165 * @param {Object} [coords] Optional. Coords of the cell before which the new row will be inserted 166 */ 167 createRow: function (coords) { 168 var row; 169 if (priv.dataType === 'array') { 170 row = []; 171 for (var c = 0; c < self.colCount; c++) { 172 row.push(null); 173 } 174 } 175 else { 176 row = $.extend(true, {}, datamap.getSchema()); 177 } 178 if (!coords || coords.row >= self.rowCount) { 179 priv.settings.data.push(row); 180 } 181 else { 182 priv.settings.data.splice(coords.row, 0, row); 183 } 184 }, 185 186 /** 187 * Creates col at the right of the data array 188 * @param {Object} [coords] Optional. Coords of the cell before which the new column will be inserted 189 */ 190 createCol: function (coords) { 191 if (priv.dataType === 'object' || priv.settings.columns) { 192 throw new Error("cannot create column with object data source or columns option specified"); 193 } 194 var r = 0; 195 if (!coords || coords.col >= self.colCount) { 196 for (; r < self.rowCount; r++) { 197 if (typeof priv.settings.data[r] === 'undefined') { 198 priv.settings.data[r] = []; 199 } 200 priv.settings.data[r].push(''); 201 } 202 } 203 else { 204 for (; r < self.rowCount; r++) { 205 priv.settings.data[r].splice(coords.col, 0, ''); 206 } 207 } 208 }, 209 210 /** 211 * Removes row at the bottom of the data array 212 * @param {Object} [coords] Optional. Coords of the cell which row will be removed 213 * @param {Object} [toCoords] Required if coords is defined. Coords of the cell until which all rows will be removed 214 */ 215 removeRow: function (coords, toCoords) { 216 if (!coords || coords.row === self.rowCount - 1) { 217 priv.settings.data.pop(); 218 } 219 else { 220 priv.settings.data.splice(coords.row, toCoords.row - coords.row + 1); 221 } 222 }, 223 224 /** 225 * Removes col at the right of the data array 226 * @param {Object} [coords] Optional. Coords of the cell which col will be removed 227 * @param {Object} [toCoords] Required if coords is defined. Coords of the cell until which all cols will be removed 228 */ 229 removeCol: function (coords, toCoords) { 230 if (priv.dataType === 'object' || priv.settings.columns) { 231 throw new Error("cannot remove column with object data source or columns option specified"); 232 } 233 var r = 0; 234 if (!coords || coords.col === self.colCount - 1) { 235 for (; r < self.rowCount; r++) { 236 priv.settings.data[r].pop(); 237 } 238 } 239 else { 240 var howMany = toCoords.col - coords.col + 1; 241 for (; r < self.rowCount; r++) { 242 priv.settings.data[r].splice(coords.col, howMany); 243 } 244 } 245 }, 246 247 /** 248 * Returns single value from the data array 249 * @param {Number} row 250 * @param {Number} prop 251 */ 252 get: function (row, prop) { 253 if (typeof prop === 'string' && prop.indexOf('.') > -1) { 254 var sliced = prop.split("."); 255 var out = priv.settings.data[row]; 256 for (var i = 0, ilen = sliced.length; i < ilen; i++) { 257 out = out[sliced[i]]; 258 if (typeof out === 'undefined') { 259 return null; 260 } 261 } 262 return out; 263 } 264 else { 265 return priv.settings.data[row] ? priv.settings.data[row][prop] : null; 266 } 267 }, 268 269 /** 270 * Saves single value to the data array 271 * @param {Number} row 272 * @param {Number} prop 273 * @param {String} value 274 */ 275 set: function (row, prop, value) { 276 if (typeof prop === 'string' && prop.indexOf('.') > -1) { 277 var sliced = prop.split("."); 278 var out = priv.settings.data[row]; 279 for (var i = 0, ilen = sliced.length - 1; i < ilen; i++) { 280 out = out[sliced[i]]; 281 } 282 out[sliced[i]] = value; 283 } 284 else { 285 priv.settings.data[row][prop] = value; 286 } 287 }, 288 289 /** 290 * Clears the data array 291 */ 292 clear: function () { 293 for (var r = 0; r < self.rowCount; r++) { 294 for (var c = 0; c < self.colCount; c++) { 295 datamap.set(r, datamap.colToProp(c), ''); 296 } 297 } 298 }, 299 300 /** 301 * Returns the data array 302 * @return {Array} 303 */ 304 getAll: function () { 305 return priv.settings.data; 306 }, 307 308 /** 309 * Returns data range as array 310 * @param {Object} start Start selection position 311 * @param {Object} end End selection position 312 * @return {Array} 313 */ 314 getRange: function (start, end) { 315 var r, rlen, c, clen, output = [], row; 316 rlen = Math.max(start.row, end.row); 317 clen = Math.max(start.col, end.col); 318 for (r = Math.min(start.row, end.row); r <= rlen; r++) { 319 row = []; 320 for (c = Math.min(start.col, end.col); c <= clen; c++) { 321 row.push(datamap.get(r, datamap.colToProp(c))); 322 } 323 output.push(row); 324 } 325 return output; 326 }, 327 328 /** 329 * Return data as text (tab separated columns) 330 * @param {Object} start (Optional) Start selection position 331 * @param {Object} end (Optional) End selection position 332 * @return {String} 333 */ 334 getText: function (start, end) { 335 return SheetClip.stringify(datamap.getRange(start, end)); 336 } 337 }; 338 339 grid = { 340 /** 341 * Alter grid 342 * @param {String} action Possible values: "insert_row", "insert_col", "remove_row", "remove_col" 343 * @param {Object} coords 344 * @param {Object} [toCoords] Required only for actions "remove_row" and "remove_col" 345 */ 346 alter: function (action, coords, toCoords) { 347 var oldData, newData, changes, r, rlen, c, clen, result; 348 oldData = $.extend(true, [], datamap.getAll()); 349 350 switch (action) { 351 case "insert_row": 352 datamap.createRow(coords); 353 self.view.createRow(coords); 354 self.blockedCols.refresh(); 355 if (priv.selStart && priv.selStart.row >= coords.row) { 356 priv.selStart.row = priv.selStart.row + 1; 357 selection.transformEnd(1, 0); 358 } 359 else { 360 selection.transformEnd(0, 0); //refresh selection, otherwise arrow movement does not work 361 } 362 break; 363 364 case "insert_col": 365 datamap.createCol(coords); 366 self.view.createCol(coords); 367 self.blockedRows.refresh(); 368 if (priv.selStart && priv.selStart.col >= coords.col) { 369 priv.selStart.col = priv.selStart.col + 1; 370 selection.transformEnd(0, 1); 371 } 372 else { 373 selection.transformEnd(0, 0); //refresh selection, otherwise arrow movement does not work 374 } 375 break; 376 377 case "remove_row": 378 datamap.removeRow(coords, toCoords); 379 self.view.removeRow(coords, toCoords); 380 result = grid.keepEmptyRows(); 381 if (!result) { 382 self.blockedCols.refresh(); 383 } 384 selection.transformEnd(0, 0); //refresh selection, otherwise arrow movement does not work 385 break; 386 387 case "remove_col": 388 datamap.removeCol(coords, toCoords); 389 self.view.removeCol(coords, toCoords); 390 result = grid.keepEmptyRows(); 391 if (!result) { 392 self.blockedRows.refresh(); 393 } 394 selection.transformEnd(0, 0); //refresh selection, otherwise arrow movement does not work 395 break; 396 } 397 398 changes = []; 399 newData = datamap.getAll(); 400 for (r = 0, rlen = newData.length; r < rlen; r++) { 401 for (c = 0, clen = newData[r].length; c < clen; c++) { 402 changes.push([r, c, oldData[r] ? oldData[r][c] : null, newData[r][c]]); 403 } 404 } 405 self.rootElement.triggerHandler("datachange.handsontable", [changes, 'alter']); 406 }, 407 408 /** 409 * Makes sure there are empty rows at the bottom of the table 410 * @return recreate {Boolean} TRUE if row or col was added or removed 411 */ 412 keepEmptyRows: function () { 413 var r, c, rlen, clen, emptyRows = 0, emptyCols = 0, recreateRows = false, recreateCols = false, val; 414 415 var $tbody = $(priv.tableBody); 416 417 //count currently empty rows 418 rows : for (r = self.countRows() - 1; r >= 0; r--) { 419 for (c = 0, clen = self.colCount; c < clen; c++) { 420 val = datamap.get(r, datamap.colToProp(c)); 421 if (val !== '' && val !== null && typeof val !== 'undefined') { 422 break rows; 423 } 424 } 425 emptyRows++; 426 } 427 428 //should I add empty rows to data source to meet startRows? 429 rlen = self.countRows(); 430 if (rlen < priv.settings.startRows) { 431 for (r = 0; r < priv.settings.startRows - rlen; r++) { 432 datamap.createRow(); 433 } 434 } 435 436 //should I add empty rows to table view to meet startRows? 437 if (self.rowCount < priv.settings.startRows) { 438 for (; self.rowCount < priv.settings.startRows; emptyRows++) { 439 self.view.createRow(); 440 recreateRows = true; 441 } 442 } 443 444 //should I add empty rows to meet minSpareRows? 445 if (emptyRows < priv.settings.minSpareRows) { 446 for (; emptyRows < priv.settings.minSpareRows; emptyRows++) { 447 datamap.createRow(); 448 self.view.createRow(); 449 recreateRows = true; 450 } 451 } 452 453 //should I add empty rows to meet minHeight 454 //WARNING! jQuery returns 0 as height() for container which is not :visible. this will lead to a infinite loop 455 if (priv.settings.minHeight) { 456 if ($tbody.height() > 0 && $tbody.height() <= priv.settings.minHeight) { 457 while ($tbody.height() <= priv.settings.minHeight) { 458 datamap.createRow(); 459 self.view.createRow(); 460 recreateRows = true; 461 } 462 } 463 } 464 465 //count currently empty cols 466 if (self.countRows() - 1 > 0) { 467 cols : for (c = self.colCount - 1; c >= 0; c--) { 468 for (r = 0; r < self.countRows(); r++) { 469 val = datamap.get(r, datamap.colToProp(c)); 470 if (val !== '' && val !== null && typeof val !== 'undefined') { 471 break cols; 472 } 473 } 474 emptyCols++; 475 } 476 } 477 478 //should I add empty cols to meet startCols? 479 if (self.colCount < priv.settings.startCols) { 480 for (; self.colCount < priv.settings.startCols; emptyCols++) { 481 if (!priv.settings.columns) { 482 datamap.createCol(); 483 } 484 self.view.createCol(); 485 recreateCols = true; 486 } 487 } 488 489 //should I add empty cols to meet minSpareCols? 490 if (priv.dataType === 'array' && emptyCols < priv.settings.minSpareCols) { 491 for (; emptyCols < priv.settings.minSpareCols; emptyCols++) { 492 if (!priv.settings.columns) { 493 datamap.createCol(); 494 } 495 self.view.createCol(); 496 recreateCols = true; 497 } 498 } 499 500 //should I add empty rows to meet minWidth 501 //WARNING! jQuery returns 0 as width() for container which is not :visible. this will lead to a infinite loop 502 if (priv.settings.minWidth) { 503 if ($tbody.width() > 0 && $tbody.width() <= priv.settings.minWidth) { 504 while ($tbody.width() <= priv.settings.minWidth) { 505 if (!priv.settings.columns) { 506 datamap.createCol(); 507 } 508 self.view.createCol(); 509 recreateCols = true; 510 } 511 } 512 } 513 514 if (!recreateRows && priv.settings.enterBeginsEditing) { 515 for (; ((priv.settings.startRows && self.rowCount > priv.settings.startRows) && (priv.settings.minSpareRows && emptyRows > priv.settings.minSpareRows) && (!priv.settings.minHeight || $tbody.height() - $tbody.find('tr:last').height() - 4 > priv.settings.minHeight)); emptyRows--) { 516 self.view.removeRow(); 517 datamap.removeRow(); 518 recreateRows = true; 519 } 520 } 521 522 if (recreateRows && priv.selStart) { 523 //if selection is outside, move selection to last row 524 if (priv.selStart.row > self.rowCount - 1) { 525 priv.selStart.row = self.rowCount - 1; 526 if (priv.selEnd.row > priv.selStart.row) { 527 priv.selEnd.row = priv.selStart.row; 528 } 529 } else if (priv.selEnd.row > self.rowCount - 1) { 530 priv.selEnd.row = self.rowCount - 1; 531 if (priv.selStart.row > priv.selEnd.row) { 532 priv.selStart.row = priv.selEnd.row; 533 } 534 } 535 } 536 537 if (priv.settings.columns && priv.settings.columns.length) { 538 clen = priv.settings.columns.length; 539 if (self.colCount !== clen) { 540 while (self.colCount > clen) { 541 self.view.removeCol(); 542 } 543 while (self.colCount < clen) { 544 self.view.createCol(); 545 } 546 recreateCols = true; 547 } 548 } 549 else if (!recreateCols && priv.settings.enterBeginsEditing) { 550 for (; ((priv.settings.startCols && self.colCount > priv.settings.startCols) && (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--) { 551 if (!priv.settings.columns) { 552 datamap.removeCol(); 553 } 554 self.view.removeCol(); 555 recreateCols = true; 556 } 557 } 558 559 if (recreateCols && priv.selStart) { 560 //if selection is outside, move selection to last row 561 if (priv.selStart.col > self.colCount - 1) { 562 priv.selStart.col = self.colCount - 1; 563 if (priv.selEnd.col > priv.selStart.col) { 564 priv.selEnd.col = priv.selStart.col; 565 } 566 } else if (priv.selEnd.col > self.colCount - 1) { 567 priv.selEnd.col = self.colCount - 1; 568 if (priv.selStart.col > priv.selEnd.col) { 569 priv.selStart.col = priv.selEnd.col; 570 } 571 } 572 } 573 574 if (recreateRows || recreateCols) { 575 selection.refreshBorders(); 576 self.blockedCols.refresh(); 577 self.blockedRows.refresh(); 578 } 579 580 return (recreateRows || recreateCols); 581 }, 582 583 /** 584 * Is cell writable 585 */ 586 isCellWritable: function ($td, cellProperties) { 587 if (priv.isPopulated) { 588 var data = $td.data('readOnly'); 589 if (typeof data === 'undefined') { 590 return !cellProperties.readOnly; 591 } 592 else { 593 return data; 594 } 595 } 596 return true; 597 }, 598 599 /** 600 * Populate cells at position with 2d array 601 * @param {Object} start Start selection position 602 * @param {Array} input 2d array 603 * @param {Object} [end] End selection position (only for drag-down mode) 604 * @param {String} [source="populateFromArray"] 605 * @return {Object|undefined} ending td in pasted area (only if any cell was changed) 606 */ 607 populateFromArray: function (start, input, end, source) { 608 var r, rlen, c, clen, td, endTd, setData = [], current = {}; 609 rlen = input.length; 610 if (rlen === 0) { 611 return false; 612 } 613 current.row = start.row; 614 current.col = start.col; 615 for (r = 0; r < rlen; r++) { 616 if ((end && current.row > end.row) || (!priv.settings.minSpareRows && current.row > self.rowCount - 1)) { 617 break; 618 } 619 current.col = start.col; 620 clen = input[r] ? input[r].length : 0; 621 for (c = 0; c < clen; c++) { 622 if ((end && current.col > end.col) || (!priv.settings.minSpareCols && current.col > self.colCount - 1)) { 623 break; 624 } 625 td = self.view.getCellAtCoords(current); 626 if (self.getCellMeta(current.row, current.col).isWritable) { 627 var p = datamap.colToProp(current.col); 628 setData.push([current.row, p, input[r][c]]); 629 } 630 current.col++; 631 if (end && c === clen - 1) { 632 c = -1; 633 } 634 } 635 current.row++; 636 if (end && r === rlen - 1) { 637 r = -1; 638 } 639 } 640 endTd = self.setDataAtCell(setData, null, null, source || 'populateFromArray'); 641 return endTd; 642 }, 643 644 /** 645 * Clears all cells in the grid 646 */ 647 clear: function () { 648 var tds = self.view.getAllCells(); 649 for (var i = 0, ilen = tds.length; i < ilen; i++) { 650 $(tds[i]).empty(); 651 self.minWidthFix(tds[i]); 652 } 653 }, 654 655 /** 656 * Returns the top left (TL) and bottom right (BR) selection coordinates 657 * @param {Object[]} coordsArr 658 * @returns {Object} 659 */ 660 getCornerCoords: function (coordsArr) { 661 function mapProp(func, array, prop) { 662 function getProp(el) { 663 return el[prop]; 664 } 665 666 if (Array.prototype.map) { 667 return func.apply(Math, array.map(getProp)); 668 } 669 return func.apply(Math, $.map(array, getProp)); 670 } 671 672 return { 673 TL: { 674 row: mapProp(Math.min, coordsArr, "row"), 675 col: mapProp(Math.min, coordsArr, "col") 676 }, 677 BR: { 678 row: mapProp(Math.max, coordsArr, "row"), 679 col: mapProp(Math.max, coordsArr, "col") 680 } 681 }; 682 }, 683 684 /** 685 * Returns array of td objects given start and end coordinates 686 */ 687 getCellsAtCoords: function (start, end) { 688 var corners = grid.getCornerCoords([start, end]); 689 var r, c, output = []; 690 for (r = corners.TL.row; r <= corners.BR.row; r++) { 691 for (c = corners.TL.col; c <= corners.BR.col; c++) { 692 output.push(self.view.getCellAtCoords({ 693 row: r, 694 col: c 695 })); 696 } 697 } 698 return output; 699 } 700 }; 701 702 this.selection = selection = { //this public assignment is only temporary 703 /** 704 * Starts selection range on given td object 705 * @param td element 706 */ 707 setRangeStart: function (td) { 708 selection.deselect(); 709 priv.selStart = self.view.getCellCoords(td); 710 selection.setRangeEnd(td); 711 }, 712 713 /** 714 * Ends selection range on given td object 715 * @param {Element} td 716 * @param {Boolean} [scrollToCell=true] If true, viewport will be scrolled to range end 717 */ 718 setRangeEnd: function (td, scrollToCell) { 719 var coords = self.view.getCellCoords(td); 720 selection.end(coords); 721 if (!priv.settings.multiSelect) { 722 priv.selStart = coords; 723 } 724 self.rootElement.triggerHandler("selection.handsontable", [priv.selStart.row, priv.selStart.col, priv.selEnd.row, priv.selEnd.col]); 725 self.rootElement.triggerHandler("selectionbyprop.handsontable", [priv.selStart.row, datamap.colToProp(priv.selStart.col), priv.selEnd.row, datamap.colToProp(priv.selEnd.col)]); 726 selection.refreshBorders(); 727 if (scrollToCell !== false) { 728 self.view.scrollViewport(td); 729 } 730 }, 731 732 /** 733 * Redraws borders around cells 734 */ 735 refreshBorders: function () { 736 editproxy.destroy(); 737 if (!selection.isSelected()) { 738 return; 739 } 740 if (autofill.handle) { 741 autofill.showHandle(); 742 } 743 priv.currentBorder.appear([priv.selStart]); 744 highlight.on(); 745 editproxy.prepare(); 746 }, 747 748 /** 749 * Setter/getter for selection start 750 */ 751 start: function (coords) { 752 if (typeof coords !== 'undefined') { 753 priv.selStart = coords; 754 } 755 return priv.selStart; 756 }, 757 758 /** 759 * Setter/getter for selection end 760 */ 761 end: function (coords) { 762 if (typeof coords !== 'undefined') { 763 priv.selEnd = coords; 764 } 765 return priv.selEnd; 766 }, 767 768 /** 769 * Returns information if we have a multiselection 770 * @return {Boolean} 771 */ 772 isMultiple: function () { 773 return !(priv.selEnd.col === priv.selStart.col && priv.selEnd.row === priv.selStart.row); 774 }, 775 776 /** 777 * Selects cell relative to current cell (if possible) 778 */ 779 transformStart: function (rowDelta, colDelta, force) { 780 if (priv.selStart.row + rowDelta > self.rowCount - 1) { 781 if (force && priv.settings.minSpareRows > 0) { 782 self.alter("insert_row", self.rowCount); 783 } 784 else if (priv.settings.autoWrapCol && priv.selStart.col + colDelta < self.colCount - 1) { 785 rowDelta = 1 - self.rowCount; 786 colDelta = 1; 787 } 788 } 789 else if (priv.settings.autoWrapCol && priv.selStart.row + rowDelta < 0 && priv.selStart.col + colDelta >= 0) { 790 rowDelta = self.rowCount - 1; 791 colDelta = -1; 792 } 793 if (priv.selStart.col + colDelta > self.colCount - 1) { 794 if (force && priv.settings.minSpareCols > 0) { 795 self.alter("insert_col", self.colCount); 796 } 797 else if (priv.settings.autoWrapRow && priv.selStart.row + rowDelta < self.rowCount - 1) { 798 rowDelta = 1; 799 colDelta = 1 - self.colCount; 800 } 801 } 802 else if (priv.settings.autoWrapRow && priv.selStart.col + colDelta < 0 && priv.selStart.row + rowDelta >= 0) { 803 rowDelta = -1; 804 colDelta = self.colCount - 1; 805 } 806 var td = self.view.getCellAtCoords({ 807 row: (priv.selStart.row + rowDelta), 808 col: priv.selStart.col + colDelta 809 }); 810 if (td) { 811 selection.setRangeStart(td); 812 } 813 else { 814 selection.setRangeStart(self.view.getCellAtCoords(priv.selStart)); //rerun some routines 815 } 816 }, 817 818 /** 819 * Sets selection end cell relative to current selection end cell (if possible) 820 */ 821 transformEnd: function (rowDelta, colDelta) { 822 if (priv.selEnd) { 823 var td = self.view.getCellAtCoords({ 824 row: (priv.selEnd.row + rowDelta), 825 col: priv.selEnd.col + colDelta 826 }); 827 if (td) { 828 selection.setRangeEnd(td); 829 } 830 } 831 }, 832 833 /** 834 * Returns true if currently there is a selection on screen, false otherwise 835 * @return {Boolean} 836 */ 837 isSelected: function () { 838 var selEnd = selection.end(); 839 if (!selEnd || typeof selEnd.row === "undefined") { 840 return false; 841 } 842 return true; 843 }, 844 845 /** 846 * Returns true if coords is within current selection coords 847 * @return {Boolean} 848 */ 849 inInSelection: function (coords) { 850 if (!selection.isSelected()) { 851 return false; 852 } 853 var sel = grid.getCornerCoords([priv.selStart, priv.selEnd]); 854 return (sel.TL.row <= coords.row && sel.BR.row >= coords.row && sel.TL.col <= coords.col && sel.BR.col >= coords.col); 855 }, 856 857 /** 858 * Deselects all selected cells 859 */ 860 deselect: function () { 861 if (!selection.isSelected()) { 862 return; 863 } 864 highlight.off(); 865 priv.currentBorder.disappear(); 866 if (autofill.handle) { 867 autofill.hideHandle(); 868 } 869 selection.end(false); 870 editproxy.destroy(); 871 self.rootElement.triggerHandler('deselect.handsontable'); 872 }, 873 874 /** 875 * Select all cells 876 */ 877 selectAll: function () { 878 if (!priv.settings.multiSelect) { 879 return; 880 } 881 var tds = self.view.getAllCells(); 882 if (tds.length) { 883 selection.setRangeStart(tds[0]); 884 selection.setRangeEnd(tds[tds.length - 1], false); 885 } 886 }, 887 888 /** 889 * Deletes data from selected cells 890 */ 891 empty: function () { 892 if (!selection.isSelected()) { 893 return; 894 } 895 var corners = grid.getCornerCoords([priv.selStart, selection.end()]); 896 var r, c, changes = []; 897 for (r = corners.TL.row; r <= corners.BR.row; r++) { 898 for (c = corners.TL.col; c <= corners.BR.col; c++) { 899 if (self.getCellMeta(r, c).isWritable) { 900 changes.push([r, datamap.colToProp(c), '']); 901 } 902 } 903 } 904 self.setDataAtCell(changes); 905 } 906 }; 907 908 highlight = { 909 /** 910 * Create highlight border 911 */ 912 init: function () { 913 priv.selectionBorder = new Handsontable.Border(self, { 914 className: 'selection', 915 bg: true 916 }); 917 }, 918 919 /** 920 * Show border around selected cells 921 */ 922 on: function () { 923 if (!selection.isSelected()) { 924 return false; 925 } 926 if (selection.isMultiple()) { 927 priv.selectionBorder.appear([priv.selStart, selection.end()]); 928 } 929 else { 930 priv.selectionBorder.disappear(); 931 } 932 }, 933 934 /** 935 * Hide border around selected cells 936 */ 937 off: function () { 938 if (!selection.isSelected()) { 939 return false; 940 } 941 priv.selectionBorder.disappear(); 942 } 943 }; 944 945 this.autofill = autofill = { //this public assignment is only temporary 946 handle: null, 947 fillBorder: null, 948 949 /** 950 * Create fill handle and fill border objects 951 */ 952 init: function () { 953 if (!autofill.handle) { 954 autofill.handle = new Handsontable.FillHandle(self); 955 autofill.fillBorder = new Handsontable.Border(self, { 956 className: 'htFillBorder' 957 }); 958 959 $(autofill.handle.handle).on('dblclick', autofill.selectAdjacent); 960 } 961 else { 962 autofill.handle.disabled = false; 963 autofill.fillBorder.disabled = false; 964 } 965 966 self.rootElement.on('beginediting.handsontable', function () { 967 autofill.hideHandle(); 968 }); 969 970 self.rootElement.on('finishediting.handsontable', function () { 971 if (selection.isSelected()) { 972 autofill.showHandle(); 973 } 974 }); 975 }, 976 977 /** 978 * Hide fill handle and fill border permanently 979 */ 980 disable: function () { 981 autofill.handle.disabled = true; 982 autofill.fillBorder.disabled = true; 983 }, 984 985 /** 986 * Selects cells down to the last row in the left column, then fills down to that cell 987 */ 988 selectAdjacent: function () { 989 var select, data, r, maxR, c; 990 991 if (selection.isMultiple()) { 992 select = priv.selectionBorder.corners; 993 } 994 else { 995 select = priv.currentBorder.corners; 996 } 997 998 autofill.fillBorder.disappear(); 999 1000 data = datamap.getAll(); 1001 rows : for (r = select.BR.row + 1; r < self.rowCount; r++) { 1002 for (c = select.TL.col; c <= select.BR.col; c++) { 1003 if (data[r][c]) { 1004 break rows; 1005 } 1006 } 1007 if (!!data[r][select.TL.col - 1] || !!data[r][select.BR.col + 1]) { 1008 maxR = r; 1009 } 1010 } 1011 if (maxR) { 1012 autofill.showBorder(self.view.getCellAtCoords({row: maxR, col: select.BR.col})); 1013 autofill.apply(); 1014 } 1015 }, 1016 1017 /** 1018 * Apply fill values to the area in fill border, omitting the selection border 1019 */ 1020 apply: function () { 1021 var drag, select, start, end; 1022 1023 autofill.handle.isDragged = 0; 1024 1025 drag = autofill.fillBorder.corners; 1026 if (!drag) { 1027 return; 1028 } 1029 1030 autofill.fillBorder.disappear(); 1031 1032 if (selection.isMultiple()) { 1033 select = priv.selectionBorder.corners; 1034 } 1035 else { 1036 select = priv.currentBorder.corners; 1037 } 1038 1039 if (drag.TL.row === select.TL.row && drag.TL.col < select.TL.col) { 1040 start = drag.TL; 1041 end = { 1042 row: drag.BR.row, 1043 col: select.TL.col - 1 1044 }; 1045 } 1046 else if (drag.TL.row === select.TL.row && drag.BR.col > select.BR.col) { 1047 start = { 1048 row: drag.TL.row, 1049 col: select.BR.col + 1 1050 }; 1051 end = drag.BR; 1052 } 1053 else if (drag.TL.row < select.TL.row && drag.TL.col === select.TL.col) { 1054 start = drag.TL; 1055 end = { 1056 row: select.TL.row - 1, 1057 col: drag.BR.col 1058 }; 1059 } 1060 else if (drag.BR.row > select.BR.row && drag.TL.col === select.TL.col) { 1061 start = { 1062 row: select.BR.row + 1, 1063 col: drag.TL.col 1064 }; 1065 end = drag.BR; 1066 } 1067 1068 if (start) { 1069 grid.populateFromArray(start, SheetClip.parse(priv.editProxy.val()), end, 'autofill'); 1070 1071 selection.setRangeStart(self.view.getCellAtCoords(drag.TL)); 1072 selection.setRangeEnd(self.view.getCellAtCoords(drag.BR)); 1073 } 1074 else { 1075 //reset to avoid some range bug 1076 selection.refreshBorders(); 1077 } 1078 }, 1079 1080 /** 1081 * Show fill handle 1082 */ 1083 showHandle: function () { 1084 autofill.handle.appear([priv.selStart, priv.selEnd]); 1085 }, 1086 1087 /** 1088 * Hide fill handle 1089 */ 1090 hideHandle: function () { 1091 autofill.handle.disappear(); 1092 }, 1093 1094 /** 1095 * Show fill border 1096 */ 1097 showBorder: function (td) { 1098 var coords = self.view.getCellCoords(td); 1099 var corners = grid.getCornerCoords([priv.selStart, priv.selEnd]); 1100 if (priv.settings.fillHandle !== 'horizontal' && (corners.BR.row < coords.row || corners.TL.row > coords.row)) { 1101 coords = {row: coords.row, col: corners.BR.col}; 1102 } 1103 else if (priv.settings.fillHandle !== 'vertical') { 1104 coords = {row: corners.BR.row, col: coords.col}; 1105 } 1106 else { 1107 return; //wrong direction 1108 } 1109 autofill.fillBorder.appear([priv.selStart, priv.selEnd, coords]); 1110 } 1111 }; 1112 1113 this.editproxy = editproxy = { //this public assignment is only temporary 1114 /** 1115 * Create input field 1116 */ 1117 init: function () { 1118 priv.editProxy = $('<textarea class="handsontableInput">'); 1119 priv.editProxyHolder = $('<div class="handsontableInputHolder">'); 1120 priv.editProxyHolder.append(priv.editProxy); 1121 1122 function onClick(event) { 1123 event.stopPropagation(); 1124 } 1125 1126 function onCut() { 1127 setTimeout(function () { 1128 selection.empty(); 1129 }, 100); 1130 } 1131 1132 function onPaste() { 1133 setTimeout(function () { 1134 var input = priv.editProxy.val().replace(/^[\r\n]*/g, '').replace(/[\r\n]*$/g, ''), //remove newline from the start and the end of the input 1135 inputArray = SheetClip.parse(input), 1136 coords = grid.getCornerCoords([priv.selStart, priv.selEnd]), 1137 endTd = grid.populateFromArray(coords.TL, inputArray, { 1138 row: Math.max(coords.BR.row, inputArray.length - 1 + coords.TL.row), 1139 col: Math.max(coords.BR.col, inputArray[0].length - 1 + coords.TL.col) 1140 }, 'paste'); 1141 if (!endTd) { 1142 endTd = self.view.getCellAtCoords(coords.BR); 1143 } 1144 selection.setRangeEnd(endTd); 1145 }, 100); 1146 } 1147 1148 var $body = $(document.body); 1149 1150 function onKeyDown(event) { 1151 if ($body.children('.context-menu-list:visible').length) { 1152 return; 1153 } 1154 1155 var r, c; 1156 priv.lastKeyCode = event.keyCode; 1157 if (selection.isSelected()) { 1158 var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; //catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) 1159 if (Handsontable.helper.isPrintableChar(event.keyCode) && ctrlDown) { 1160 if (event.keyCode === 65) { //CTRL + A 1161 selection.selectAll(); //select all cells 1162 } 1163 else if (event.keyCode === 88 && $.browser.opera) { //CTRL + X 1164 priv.editProxyHolder.triggerHandler('cut'); //simulate oncut for Opera 1165 } 1166 else if (event.keyCode === 86 && $.browser.opera) { //CTRL + V 1167 priv.editProxyHolder.triggerHandler('paste'); //simulate onpaste for Opera 1168 } 1169 else if (event.keyCode === 89 || (event.shiftKey && event.keyCode === 90)) { //CTRL + Y or CTRL + SHIFT + Z 1170 priv.undoRedo && priv.undoRedo.redo(); 1171 } 1172 else if (event.keyCode === 90) { //CTRL + Z 1173 priv.undoRedo && priv.undoRedo.undo(); 1174 } 1175 return; 1176 } 1177 1178 var rangeModifier = event.shiftKey ? selection.setRangeEnd : selection.setRangeStart; 1179 1180 switch (event.keyCode) { 1181 case 38: /* arrow up */ 1182 if (event.shiftKey) { 1183 selection.transformEnd(-1, 0); 1184 } 1185 else { 1186 selection.transformStart(-1, 0); 1187 } 1188 event.preventDefault(); 1189 break; 1190 1191 case 9: /* tab */ 1192 r = priv.settings.tabMoves.row; 1193 c = priv.settings.tabMoves.col; 1194 if (event.shiftKey) { 1195 selection.transformStart(-r, -c); 1196 } 1197 else { 1198 selection.transformStart(r, c); 1199 } 1200 event.preventDefault(); 1201 break; 1202 1203 case 39: /* arrow right */ 1204 if (event.shiftKey) { 1205 selection.transformEnd(0, 1); 1206 } 1207 else { 1208 selection.transformStart(0, 1); 1209 } 1210 event.preventDefault(); 1211 break; 1212 1213 case 37: /* arrow left */ 1214 if (event.shiftKey) { 1215 selection.transformEnd(0, -1); 1216 } 1217 else { 1218 selection.transformStart(0, -1); 1219 } 1220 event.preventDefault(); 1221 break; 1222 1223 case 8: /* backspace */ 1224 case 46: /* delete */ 1225 selection.empty(event); 1226 event.preventDefault(); 1227 break; 1228 1229 case 40: /* arrow down */ 1230 if (event.shiftKey) { 1231 selection.transformEnd(1, 0); //expanding selection down with shift 1232 } 1233 else { 1234 selection.transformStart(1, 0); //move selection down 1235 } 1236 event.preventDefault(); 1237 break; 1238 1239 case 113: /* F2 */ 1240 event.preventDefault(); //prevent Opera from opening Go to Page dialog 1241 break; 1242 1243 case 13: /* return/enter */ 1244 r = priv.settings.enterMoves.row; 1245 c = priv.settings.enterMoves.col; 1246 if (event.shiftKey) { 1247 selection.transformStart(-r, -c); //move selection up 1248 } 1249 else { 1250 selection.transformStart(r, c); //move selection down 1251 } 1252 event.preventDefault(); //don't add newline to field 1253 break; 1254 1255 case 36: /* home */ 1256 if (event.ctrlKey || event.metaKey) { 1257 rangeModifier(self.view.getCellAtCoords({row: 0, col: priv.selStart.col})); 1258 } 1259 else { 1260 rangeModifier(self.view.getCellAtCoords({row: priv.selStart.row, col: 0})); 1261 } 1262 break; 1263 1264 case 35: /* end */ 1265 if (event.ctrlKey || event.metaKey) { 1266 rangeModifier(self.view.getCellAtCoords({row: self.rowCount - 1, col: priv.selStart.col})); 1267 } 1268 else { 1269 rangeModifier(self.view.getCellAtCoords({row: priv.selStart.row, col: self.colCount - 1})); 1270 } 1271 break; 1272 1273 case 33: /* pg up */ 1274 rangeModifier(self.view.getCellAtCoords({row: 0, col: priv.selStart.col})); 1275 break; 1276 1277 case 34: /* pg dn */ 1278 rangeModifier(self.view.getCellAtCoords({row: self.rowCount - 1, col: priv.selStart.col})); 1279 break; 1280 1281 default: 1282 break; 1283 } 1284 } 1285 } 1286 1287 priv.editProxy.on('click', onClick); 1288 priv.editProxyHolder.on('cut', onCut); 1289 priv.editProxyHolder.on('paste', onPaste); 1290 priv.editProxyHolder.on('keydown', onKeyDown); 1291 self.container.append(priv.editProxyHolder); 1292 }, 1293 1294 /** 1295 * Destroy current editor, if exists 1296 */ 1297 destroy: function () { 1298 if (typeof priv.editorDestroyer === "function") { 1299 priv.editorDestroyer(); 1300 priv.editorDestroyer = null; 1301 } 1302 }, 1303 1304 /** 1305 * Prepare text input to be displayed at given grid cell 1306 */ 1307 prepare: function () { 1308 priv.editProxy.height(priv.editProxy.parent().innerHeight() - 4); 1309 priv.editProxy.val(datamap.getText(priv.selStart, priv.selEnd)); 1310 setTimeout(editproxy.focus, 1); 1311 priv.editorDestroyer = self.view.applyCellTypeMethod('editor', self.view.getCellAtCoords(priv.selStart), priv.selStart, priv.editProxy); 1312 }, 1313 1314 /** 1315 * Sets focus to textarea 1316 */ 1317 focus: function () { 1318 priv.editProxy[0].select(); 1319 } 1320 }; 1321 1322 this.init = function () { 1323 this.view = new Handsontable.TableView(this); 1324 1325 if (typeof settings.cols !== 'undefined') { 1326 settings.startCols = settings.cols; //backwards compatibility 1327 } 1328 1329 self.colCount = settings.startCols; 1330 self.rowCount = 0; 1331 1332 highlight.init(); 1333 priv.currentBorder = new Handsontable.Border(self, { 1334 className: 'current', 1335 bg: true 1336 }); 1337 editproxy.init(); 1338 1339 bindEvents(); 1340 this.updateSettings(settings); 1341 1342 Handsontable.PluginHooks.run(self, 'afterInit'); 1343 }; 1344 1345 var bindEvents = function () { 1346 self.rootElement.on("beforedatachange.handsontable", function (event, changes) { 1347 if (priv.settings.autoComplete) { //validate strict autocompletes 1348 var typeahead = priv.editProxy.data('typeahead'); 1349 loop : for (var c = changes.length - 1; c >= 0; c--) { 1350 for (var a = 0, alen = priv.settings.autoComplete.length; a < alen; a++) { 1351 var autoComplete = priv.settings.autoComplete[a]; 1352 var source = autoComplete.source(); 1353 if (changes[c][3] && autoComplete.match(changes[c][0], changes[c][1], datamap.getAll)) { 1354 var lowercaseVal = changes[c][3].toLowerCase(); 1355 for (var s = 0, slen = source.length; s < slen; s++) { 1356 if (changes[c][3] === source[s]) { 1357 continue loop; //perfect match 1358 } 1359 else if (lowercaseVal === source[s].toLowerCase()) { 1360 changes[c][3] = source[s]; //good match, fix the case 1361 continue loop; 1362 } 1363 } 1364 if (autoComplete.strict) { 1365 changes.splice(c, 1); //no match, invalidate this change 1366 continue loop; 1367 } 1368 } 1369 } 1370 } 1371 } 1372 1373 if (priv.settings.onBeforeChange) { 1374 var result = priv.settings.onBeforeChange.apply(self.rootElement[0], [changes]); 1375 if (result === false) { 1376 changes.splice(0, changes.length); //invalidate all changes (remove everything from array) 1377 } 1378 } 1379 }); 1380 self.rootElement.on("datachange.handsontable", function (event, changes, source) { 1381 if (priv.settings.onChange) { 1382 priv.settings.onChange.apply(self.rootElement[0], [changes, source]); 1383 } 1384 }); 1385 self.rootElement.on("selection.handsontable", function (event, row, col, endRow, endCol) { 1386 if (priv.settings.onSelection) { 1387 priv.settings.onSelection.apply(self.rootElement[0], [row, col, endRow, endCol]); 1388 } 1389 }); 1390 self.rootElement.on("selectionbyprop.handsontable", function (event, row, prop, endRow, endProp) { 1391 if (priv.settings.onSelectionByProp) { 1392 priv.settings.onSelectionByProp.apply(self.rootElement[0], [row, prop, endRow, endProp]); 1393 } 1394 }); 1395 }; 1396 1397 /** 1398 * Set data at given cell 1399 * @public 1400 * @param {Number|Array} row or array of changes in format [[row, col, value], ...] 1401 * @param {Number} prop 1402 * @param {String} value 1403 * @param {String} [source='edit'] String that identifies how this change will be described in changes array (useful in onChange callback) 1404 */ 1405 this.setDataAtCell = function (row, prop, value, source) { 1406 var refreshRows = false, refreshCols = false, changes, i, ilen, td, changesByCol = []; 1407 1408 if (typeof row === "object") { //is it an array of changes 1409 changes = row; 1410 } 1411 else if ($.isPlainObject(value)) { //backwards compatibility 1412 changes = value; 1413 } 1414 else { 1415 changes = [ 1416 [row, prop, value] 1417 ]; 1418 } 1419 1420 for (i = 0, ilen = changes.length; i < ilen; i++) { 1421 changes[i].splice(2, 0, datamap.get(changes[i][0], changes[i][1])); //add old value at index 2 1422 } 1423 1424 self.rootElement.triggerHandler("beforedatachange.handsontable", [changes]); 1425 1426 for (i = 0, ilen = changes.length; i < ilen; i++) { 1427 row = changes[i][0]; 1428 prop = changes[i][1]; 1429 var col = datamap.propToCol(prop); 1430 value = changes[i][3]; 1431 changesByCol.push([changes[i][0], col, changes[i][2], changes[i][3], changes[i][4]]); 1432 1433 if (priv.settings.minSpareRows) { 1434 while (row > self.rowCount - 1) { 1435 datamap.createRow(); 1436 self.view.createRow(); 1437 refreshRows = true; 1438 } 1439 } 1440 if (priv.dataType === 'array' && priv.settings.minSpareCols) { 1441 while (col > self.colCount - 1) { 1442 datamap.createCol(); 1443 self.view.createCol(); 1444 refreshCols = true; 1445 } 1446 } 1447 td = self.view.render(row, col, prop, value); 1448 datamap.set(row, prop, value); 1449 } 1450 if (refreshRows) { 1451 self.blockedCols.refresh(); 1452 } 1453 if (refreshCols) { 1454 self.blockedRows.refresh(); 1455 } 1456 var recreated = grid.keepEmptyRows(); 1457 if (!recreated) { 1458 selection.refreshBorders(); 1459 } 1460 if (changes.length) { 1461 self.rootElement.triggerHandler("datachange.handsontable", [changes, source || 'edit']); 1462 self.rootElement.triggerHandler("cellrender.handsontable", [changes, source || 'edit']); 1463 } 1464 return td; 1465 }; 1466 1467 /** 1468 * Populate cells at position with 2d array 1469 * @param {Object} start Start selection position 1470 * @param {Array} input 2d array 1471 * @param {Object} [end] End selection position (only for drag-down mode) 1472 * @param {String} [source="populateFromArray"] 1473 * @return {Object|undefined} ending td in pasted area (only if any cell was changed) 1474 */ 1475 this.populateFromArray = function (start, input, end, source) { 1476 return grid.populateFromArray(start, input, end, source); 1477 }; 1478 1479 /** 1480 * Returns the top left (TL) and bottom right (BR) selection coordinates 1481 * @param {Object[]} coordsArr 1482 * @returns {Object} 1483 */ 1484 this.getCornerCoords = function (coordsArr) { 1485 return grid.getCornerCoords(coordsArr); 1486 }; 1487 1488 /** 1489 * Returns current selection. Returns undefined if there is no selection. 1490 * @public 1491 * @return {Array} [topLeftRow, topLeftCol, bottomRightRow, bottomRightCol] 1492 */ 1493 this.getSelected = function () { //https://github.com/warpech/jquery-handsontable/issues/44 //cjl 1494 if (selection.isSelected()) { 1495 var coords = grid.getCornerCoords([priv.selStart, priv.selEnd]); 1496 return [coords.TL.row, coords.TL.col, coords.BR.row, coords.BR.col]; 1497 } 1498 }; 1499 1500 /** 1501 * Render visible data 1502 * @public 1503 * @param {Array} changes (Optional) If not given, all visible grid will be rerendered 1504 * @param {String} source (Optional) 1505 */ 1506 this.render = function (changes, source) { 1507 if (typeof changes === "undefined") { 1508 changes = []; 1509 var r, c, p, val, clen = (priv.settings.columns && priv.settings.columns.length) || priv.settings.startCols; 1510 for (r = 0; r < priv.settings.startRows; r++) { 1511 for (c = 0; c < clen; c++) { 1512 p = datamap.colToProp(c); 1513 val = datamap.get(r, p); 1514 changes.push([r, p, val, val]); 1515 } 1516 } 1517 } 1518 for (var i = 0, ilen = changes.length; i < ilen; i++) { 1519 self.view.render(changes[i][0], datamap.propToCol(changes[i][1]), changes[i][1], changes[i][3]); 1520 } 1521 self.rootElement.triggerHandler('cellrender.handsontable', [changes, source || 'render']); 1522 }; 1523 1524 /** 1525 * Load data from array 1526 * @public 1527 * @param {Array} data 1528 */ 1529 this.loadData = function (data) { 1530 priv.isPopulated = false; 1531 priv.settings.data = data; 1532 if ($.isPlainObject(priv.settings.dataSchema) || $.isPlainObject(data[0])) { 1533 priv.dataType = 'object'; 1534 } 1535 else { 1536 priv.dataType = 'array'; 1537 } 1538 if(data[0]) { 1539 priv.duckDataSchema = datamap.recursiveDuckSchema(data[0]); 1540 } 1541 else { 1542 priv.duckDataSchema = {}; 1543 } 1544 datamap.createMap(); 1545 var dlen = priv.settings.data.length; 1546 while (priv.settings.startRows > dlen) { 1547 datamap.createRow(); 1548 dlen++; 1549 } 1550 while (self.rowCount < dlen) { 1551 self.view.createRow(); 1552 } 1553 1554 grid.keepEmptyRows(); 1555 grid.clear(); 1556 var changes = []; 1557 var clen = (priv.settings.columns && priv.settings.columns.length) || priv.settings.startCols; 1558 for (var r = 0; r < dlen; r++) { 1559 for (var c = 0; c < clen; c++) { 1560 var p = datamap.colToProp(c); 1561 changes.push([r, p, "", datamap.get(r, p)]) 1562 } 1563 } 1564 self.rootElement.triggerHandler('datachange.handsontable', [changes, 'loadData']); 1565 self.render(changes, 'loadData'); 1566 priv.isPopulated = true; 1567 self.clearUndo(); 1568 }; 1569 1570 /** 1571 * Return the current data object (the same that was passed by `data` configuration option or `loadData` method). Optionally you can provide cell range `r`, `c`, `r2`, `c2` to get only a fragment of grid data 1572 * @public 1573 * @param {Number} r (Optional) From row 1574 * @param {Number} c (Optional) From col 1575 * @param {Number} r2 (Optional) To row 1576 * @param {Number} c2 (Optional) To col 1577 * @return {Array|Object} 1578 */ 1579 this.getData = function (r, c, r2, c2) { 1580 if (typeof r === 'undefined') { 1581 return datamap.getAll(); 1582 } 1583 else { 1584 return datamap.getRange({row: r, col: c}, {row: r2, col: c2}); 1585 } 1586 }; 1587 1588 /** 1589 * Update settings 1590 * @public 1591 */ 1592 this.updateSettings = function (settings) { 1593 var i, j, recreated; 1594 1595 if (typeof settings.rows !== "undefined") { 1596 settings.startRows = settings.rows; //backwards compatibility 1597 } 1598 if (typeof settings.cols !== "undefined") { 1599 settings.startCols = settings.cols; //backwards compatibility 1600 } 1601 1602 if (typeof settings.fillHandle !== "undefined") { 1603 if (autofill.handle && settings.fillHandle === false) { 1604 autofill.disable(); 1605 } 1606 else if (!autofill.handle && settings.fillHandle !== false) { 1607 autofill.init(); 1608 } 1609 } 1610 1611 if (typeof settings.undo !== "undefined") { 1612 if (priv.undoRedo && settings.undo === false) { 1613 priv.undoRedo = null; 1614 } 1615 else if (!priv.undoRedo && settings.undo === true) { 1616 priv.undoRedo = new Handsontable.UndoRedo(self); 1617 } 1618 } 1619 1620 if (!self.blockedCols) { 1621 self.blockedCols = new Handsontable.BlockedCols(self); 1622 self.blockedRows = new Handsontable.BlockedRows(self); 1623 } 1624 1625 for (i in settings) { 1626 if (i === 'data') { 1627 continue; //loadData will be triggered later 1628 } 1629 else if (settings.hasOwnProperty(i)) { 1630 priv.settings[i] = settings[i]; 1631 1632 //launch extensions 1633 if (Handsontable.extension[i]) { 1634 priv.extensions[i] = new Handsontable.extension[i](self, settings[i]); 1635 } 1636 } 1637 } 1638 1639 if (typeof settings.colHeaders !== "undefined") { 1640 if (settings.colHeaders === false && priv.extensions["ColHeader"]) { 1641 priv.extensions["ColHeader"].destroy(); 1642 } 1643 else if (settings.colHeaders !== false) { 1644 priv.extensions["ColHeader"] = new Handsontable.ColHeader(self, settings.colHeaders); 1645 } 1646 } 1647 1648 if (typeof settings.rowHeaders !== "undefined") { 1649 if (settings.rowHeaders === false && priv.extensions["RowHeader"]) { 1650 priv.extensions["RowHeader"].destroy(); 1651 } 1652 else if (settings.rowHeaders !== false) { 1653 priv.extensions["RowHeader"] = new Handsontable.RowHeader(self, settings.rowHeaders); 1654 } 1655 } 1656 1657 var blockedRowsCount = self.blockedRows.count(); 1658 var blockedColsCount = self.blockedCols.count(); 1659 if (blockedRowsCount && blockedColsCount && (typeof settings.rowHeaders !== "undefined" || typeof settings.colHeaders !== "undefined")) { 1660 if (self.blockedCorner) { 1661 self.blockedCorner.remove(); 1662 self.blockedCorner = null; 1663 } 1664 1665 var position = self.table.position(); 1666 self.positionFix(position); 1667 1668 var div = document.createElement('div'); 1669 div.style.position = 'absolute'; 1670 div.style.top = position.top + 'px'; 1671 div.style.left = position.left + 'px'; 1672 1673 var table = document.createElement('table'); 1674 table.cellPadding = 0; 1675 table.cellSpacing = 0; 1676 div.appendChild(table); 1677 1678 var thead = document.createElement('thead'); 1679 table.appendChild(thead); 1680 1681 var tr, th; 1682 for (i = 0; i < blockedRowsCount; i++) { 1683 tr = document.createElement('tr'); 1684 for (j = blockedColsCount - 1; j >= 0; j--) { 1685 th = document.createElement('th'); 1686 th.className = self.blockedCols.headers[j].className; 1687 th.innerHTML = self.blockedCols.headerText(' '); 1688 self.minWidthFix(th); 1689 tr.appendChild(th); 1690 } 1691 thead.appendChild(tr); 1692 } 1693 self.blockedCorner = $(div); 1694 self.blockedCorner.on('click', function () { 1695 selection.selectAll(); 1696 }); 1697 self.container.append(self.blockedCorner); 1698 } 1699 else { 1700 if (self.blockedCorner) { 1701 self.blockedCorner.remove(); 1702 self.blockedCorner = null; 1703 } 1704 } 1705 1706 if (typeof settings.data !== 'undefined') { 1707 self.loadData(settings.data); 1708 recreated = true; 1709 } 1710 else if (typeof settings.columns !== "undefined") { 1711 datamap.createMap(); 1712 } 1713 1714 if (!recreated) { 1715 recreated = grid.keepEmptyRows(); 1716 } 1717 1718 if (!recreated) { 1719 selection.refreshBorders(); 1720 } 1721 1722 self.blockedCols.update(); 1723 self.blockedRows.update(); 1724 }; 1725 1726 /** 1727 * Returns current settings object 1728 * @return {Object} 1729 */ 1730 this.getSettings = function () { 1731 return priv.settings; 1732 }; 1733 1734 /** 1735 * Clears grid 1736 * @public 1737 */ 1738 this.clear = function () { 1739 selection.selectAll(); 1740 selection.empty(); 1741 }; 1742 1743 /** 1744 * Return true if undo can be performed, false otherwise 1745 * @public 1746 */ 1747 this.isUndoAvailable = function () { 1748 return priv.undoRedo && priv.undoRedo.isUndoAvailable(); 1749 }; 1750 1751 /** 1752 * Return true if redo can be performed, false otherwise 1753 * @public 1754 */ 1755 this.isRedoAvailable = function () { 1756 return priv.undoRedo && priv.undoRedo.isRedoAvailable(); 1757 }; 1758 1759 /** 1760 * Undo last edit 1761 * @public 1762 */ 1763 this.undo = function () { 1764 priv.undoRedo && priv.undoRedo.undo(); 1765 }; 1766 1767 /** 1768 * Redo edit (used to reverse an undo) 1769 * @public 1770 */ 1771 this.redo = function () { 1772 priv.undoRedo && priv.undoRedo.redo(); 1773 }; 1774 1775 /** 1776 * Clears undo history 1777 * @public 1778 */ 1779 this.clearUndo = function () { 1780 priv.undoRedo && priv.undoRedo.clear(); 1781 }; 1782 1783 /** 1784 * Alters the grid 1785 * @param {String} action See grid.alter for possible values 1786 * @param {Number} from 1787 * @param {Number} [to] Optional. Used only for actions "remove_row" and "remove_col" 1788 * @public 1789 */ 1790 this.alter = function (action, from, to) { 1791 if (typeof to === "undefined") { 1792 to = from; 1793 } 1794 switch (action) { 1795 case "insert_row": 1796 case "remove_row": 1797 grid.alter(action, {row: from, col: 0}, {row: to, col: 0}); 1798 break; 1799 1800 case "insert_col": 1801 case "remove_col": 1802 grid.alter(action, {row: 0, col: from}, {row: 0, col: to}); 1803 break; 1804 1805 default: 1806 throw Error('There is no such action "' + action + '"'); 1807 break; 1808 } 1809 }; 1810 1811 /** 1812 * Returns <td> element corresponding to params row, col 1813 * @param {Number} row 1814 * @param {Number} col 1815 * @public 1816 * @return {Element} 1817 */ 1818 this.getCell = function (row, col) { 1819 return self.view.getCellAtCoords({row: row, col: col}); 1820 }; 1821 1822 /** 1823 * Returns property name associated with column number 1824 * @param {Number} col 1825 * @public 1826 * @return {String} 1827 */ 1828 this.colToProp = function (col) { 1829 return datamap.colToProp(col); 1830 }; 1831 1832 /** 1833 * Returns column number associated with property name 1834 * @param {String} prop 1835 * @public 1836 * @return {Number} 1837 */ 1838 this.propToCol = function (prop) { 1839 return datamap.propToCol(prop); 1840 }; 1841 1842 /** 1843 * Return cell value at `row`, `col` 1844 * @param {Number} row 1845 * @param {Number} col 1846 * @public 1847 * @return {string} 1848 */ 1849 this.getDataAtCell = function (row, col) { 1850 return datamap.get(row, datamap.colToProp(col)); 1851 }; 1852 1853 /** 1854 * Returns cell meta data object corresponding to params row, col 1855 * @param {Number} row 1856 * @param {Number} col 1857 * @public 1858 * @return {Object} 1859 */ 1860 this.getCellMeta = function (row, col) { 1861 var cellProperites = {} 1862 , prop = datamap.colToProp(col); 1863 if (priv.settings.columns) { 1864 cellProperites = $.extend(true, cellProperites, priv.settings.columns[col] || {}); 1865 } 1866 if (priv.settings.cells) { 1867 cellProperites = $.extend(true, cellProperites, priv.settings.cells(row, col, prop) || {}); 1868 } 1869 cellProperites.isWritable = grid.isCellWritable($(self.view.getCellAtCoords({row: row, col: col})), cellProperites); 1870 return cellProperites; 1871 }; 1872 1873 /** 1874 * Sets cell to be readonly 1875 * @param {Number} row 1876 * @param {Number} col 1877 * @public 1878 */ 1879 this.setCellReadOnly = function (row, col) { 1880 $(self.view.getCellAtCoords({row: row, col: col})).data("readOnly", true); 1881 }; 1882 1883 /** 1884 * Sets cell to be editable (removes readonly) 1885 * @param {Number} row 1886 * @param {Number} col 1887 * @public 1888 */ 1889 this.setCellEditable = function (row, col) { 1890 $(self.view.getCellAtCoords({row: row, col: col})).data("readOnly", false); 1891 }; 1892 1893 /** 1894 * Returns headers (if they are enabled) 1895 * @param {Object} obj Instance of rowHeader or colHeader 1896 * @param {Number} count Number of rows or cols 1897 * @param {Number} index (Optional) Will return only header at given index 1898 * @return {Array|String} 1899 */ 1900 var getHeaderText = function (obj, count, index) { 1901 if (obj) { 1902 if (typeof index !== 'undefined') { 1903 return obj.columnLabel(index); 1904 } 1905 else { 1906 var headers = []; 1907 for (var i = 0; i < count; i++) { 1908 headers.push(obj.columnLabel(i)); 1909 } 1910 return headers; 1911 } 1912 } 1913 }; 1914 1915 /** 1916 * Return array of row headers (if they are enabled). If param `row` given, return header at given row as string 1917 * @param {Number} row (Optional) 1918 * @return {Array|String} 1919 */ 1920 this.getRowHeader = function (row) { 1921 return getHeaderText(self.rowHeader, self.rowCount, row); 1922 }; 1923 1924 /** 1925 * Return array of col headers (if they are enabled). If param `col` given, return header at given col as string 1926 * @param {Number} col (Optional) 1927 * @return {Array|String} 1928 */ 1929 this.getColHeader = function (col) { 1930 return getHeaderText(self.colHeader, self.colCount, col); 1931 }; 1932 1933 /** 1934 * Return total number of rows in grid 1935 * @return {Number} 1936 */ 1937 this.countRows = function () { 1938 return priv.settings.data.length; 1939 }; 1940 1941 /** 1942 * Return total number of columns in grid 1943 * @return {Number} 1944 */ 1945 this.countCols = function () { 1946 return self.colCount; 1947 }; 1948 1949 /** 1950 * Selects cell on grid. Optionally selects range to another cell 1951 * @param {Number} row 1952 * @param {Number} col 1953 * @param {Number} [endRow] 1954 * @param {Number} [endCol] 1955 * @param {Boolean} [scrollToCell=true] If true, viewport will be scrolled to the selection 1956 * @public 1957 */ 1958 this.selectCell = function (row, col, endRow, endCol, scrollToCell) { 1959 if (typeof row !== 'number' || row < 0 || row >= self.rowCount) { 1960 return false; 1961 } 1962 if (typeof col !== 'number' || col < 0 || col >= self.colCount) { 1963 return false; 1964 } 1965 if (typeof endRow !== "undefined") { 1966 if (typeof endRow !== 'number' || endRow < 0 || endRow >= self.rowCount) { 1967 return false; 1968 } 1969 if (typeof endCol !== 'number' || endCol < 0 || endCol >= self.colCount) { 1970 return false; 1971 } 1972 } 1973 selection.start({row: row, col: col}); 1974 if (typeof endRow === "undefined") { 1975 selection.setRangeEnd(self.getCell(row, col), scrollToCell); 1976 } 1977 else { 1978 selection.setRangeEnd(self.getCell(endRow, endCol), scrollToCell); 1979 } 1980 }; 1981 1982 this.selectCellByProp = function (row, prop, endRow, endProp, scrollToCell) { 1983 arguments[1] = datamap.propToCol(arguments[1]); 1984 if (typeof arguments[3] !== "undefined") { 1985 arguments[3] = datamap.propToCol(arguments[3]); 1986 } 1987 return self.selectCell.apply(self, arguments); 1988 }; 1989 1990 /** 1991 * Deselects current sell selection on grid 1992 * @public 1993 */ 1994 this.deselectCell = function () { 1995 selection.deselect(); 1996 }; 1997 1998 /** 1999 * Remove grid from DOM 2000 * @public 2001 */ 2002 this.destroy = function () { 2003 self.rootElement.empty(); 2004 self.rootElement.removeData('handsontable'); 2005 }; 2006 }; 2007 2008 var settings = { 2009 'data': [], 2010 'startRows': 5, 2011 'startCols': 5, 2012 'minSpareRows': 0, 2013 'minSpareCols': 0, 2014 'minHeight': 0, 2015 'minWidth': 0, 2016 'multiSelect': true, 2017 'fillHandle': true, 2018 'undo': true, 2019 'outsideClickDeselects': true, 2020 'enterBeginsEditing': true, 2021 'enterMoves': {row: 1, col: 0}, 2022 'tabMoves': {row: 0, col: 1}, 2023 'autoWrapRow': false, 2024 'autoWrapCol': false 2025 }; 2026 2027 $.fn.handsontable = function (action, options) { 2028 var i, ilen, args, output = []; 2029 if (typeof action !== 'string') { //init 2030 options = action; 2031 return this.each(function () { 2032 var $this = $(this); 2033 if ($this.data("handsontable")) { 2034 instance = $this.data("handsontable"); 2035 instance.updateSettings(options); 2036 } 2037 else { 2038 var currentSettings = $.extend(true, {}, settings), instance; 2039 for (i in options) { 2040 if (options.hasOwnProperty(i)) { 2041 currentSettings[i] = options[i]; 2042 } 2043 } 2044 instance = new Handsontable.Core($this, currentSettings); 2045 $this.data("handsontable", instance); 2046 instance.init(); 2047 } 2048 }); 2049 } 2050 else { 2051 args = []; 2052 if (arguments.length > 1) { 2053 for (i = 1, ilen = arguments.length; i < ilen; i++) { 2054 args.push(arguments[i]); 2055 } 2056 } 2057 this.each(function () { 2058 output = $(this).data("handsontable")[action].apply(this, args); 2059 }); 2060 return output; 2061 } 2062 }; 2063 /** 2064 * Handsontable TableView constructor 2065 * @param {Object} instance 2066 */ 2067 Handsontable.TableView = function (instance) { 2068 var that = this; 2069 this.instance = instance; 2070 var priv = {}; 2071 2072 var interaction = { 2073 onMouseDown: function (event) { 2074 priv.isMouseDown = true; 2075 if (event.button === 2 && that.instance.selection.inInSelection(that.getCellCoords(this))) { //right mouse button 2076 //do nothing 2077 } 2078 else if (event.shiftKey) { 2079 that.instance.selection.setRangeEnd(this); 2080 } 2081 else { 2082 that.instance.selection.setRangeStart(this); 2083 } 2084 }, 2085 2086 onMouseOver: function () { 2087 if (priv.isMouseDown) { 2088 that.instance.selection.setRangeEnd(this); 2089 } 2090 else if (that.instance.autofill.handle && that.instance.autofill.handle.isDragged) { 2091 that.instance.autofill.handle.isDragged++; 2092 that.instance.autofill.showBorder(this); 2093 } 2094 }, 2095 2096 onMouseWheel: function (event, delta, deltaX, deltaY) { 2097 if (priv.virtualScroll) { 2098 if (deltaY) { 2099 priv.virtualScroll.scrollTop(priv.virtualScroll.scrollTop() + 44 * -deltaY); 2100 } 2101 else if (deltaX) { 2102 priv.virtualScroll.scrollLeft(priv.virtualScroll.scrollLeft() + 100 * deltaX); 2103 } 2104 event.preventDefault(); 2105 } 2106 } 2107 }; 2108 2109 2110 that.instance.container = $('<div class="handsontable"></div>'); 2111 var overflow = that.instance.rootElement.css('overflow'); 2112 if (overflow === 'auto' || overflow === 'scroll') { 2113 that.instance.container[0].style.overflow = overflow; 2114 var w = that.instance.rootElement.css('width'); 2115 if (w) { 2116 that.instance.container[0].style.width = w; 2117 } 2118 var h = that.instance.rootElement.css('height'); 2119 if (h) { 2120 that.instance.container[0].style.height = h; 2121 } 2122 that.instance.rootElement[0].style.overflow = 'hidden'; 2123 that.instance.rootElement[0].style.position = 'relative'; 2124 } 2125 that.instance.rootElement.append(that.instance.container); 2126 2127 //this.init 2128 2129 function onMouseEnterTable() { 2130 priv.isMouseOverTable = true; 2131 } 2132 2133 function onMouseLeaveTable() { 2134 priv.isMouseOverTable = false; 2135 } 2136 2137 that.instance.curScrollTop = that.instance.curScrollLeft = 0; 2138 that.instance.lastScrollTop = that.instance.lastScrollLeft = null; 2139 this.scrollbarSize = this.measureScrollbar(); 2140 2141 var div = $('<div><table class="htCore" cellspacing="0" cellpadding="0"><thead></thead><tbody></tbody></table></div>'); 2142 priv.tableContainer = div[0]; 2143 that.instance.table = $(priv.tableContainer.firstChild); 2144 this.$tableBody = that.instance.table.find("tbody")[0]; 2145 that.instance.table.on('mousedown', 'td', interaction.onMouseDown); 2146 that.instance.table.on('mouseover', 'td', interaction.onMouseOver); 2147 that.instance.table.on('mousewheel', 'td', interaction.onMouseWheel); 2148 that.instance.container.append(div); 2149 2150 //... 2151 2152 2153 that.instance.container.on('mouseenter', onMouseEnterTable).on('mouseleave', onMouseLeaveTable); 2154 2155 2156 function onMouseUp() { 2157 if (priv.isMouseDown) { 2158 setTimeout(that.instance.editproxy.focus, 1); 2159 } 2160 priv.isMouseDown = false; 2161 if (that.instance.autofill.handle && that.instance.autofill.handle.isDragged) { 2162 if (that.instance.autofill.handle.isDragged > 1) { 2163 that.instance.autofill.apply(); 2164 } 2165 that.instance.autofill.handle.isDragged = 0; 2166 } 2167 } 2168 2169 function onOutsideClick(event) { 2170 if (that.instance.getSettings().outsideClickDeselects) { 2171 setTimeout(function () {//do async so all mouseenter, mouseleave events will fire before 2172 if (!priv.isMouseOverTable && event.target !== priv.tableContainer && $(event.target).attr('id') !== 'context-menu-layer') { //if clicked outside the table or directly at container which also means outside 2173 that.instance.selection.deselect(); 2174 } 2175 }, 1); 2176 } 2177 } 2178 2179 $("html").on('mouseup', onMouseUp). 2180 on('click', onOutsideClick); 2181 2182 if (that.instance.container[0].tagName.toLowerCase() !== "html" && that.instance.container[0].tagName.toLowerCase() !== "body" && (that.instance.container.css('overflow') === 'scroll' || that.instance.container.css('overflow') === 'auto')) { 2183 that.scrollable = that.instance.container; 2184 } 2185 2186 if (that.scrollable) { 2187 //create fake scrolling div 2188 priv.virtualScroll = $('<div class="virtualScroll"><div class="spacer"></div></div>'); 2189 that.scrollable = priv.virtualScroll; 2190 that.instance.container.before(priv.virtualScroll); 2191 that.instance.table[0].style.position = 'absolute'; 2192 priv.virtualScroll.css({ 2193 width: that.instance.container.width() + 'px', 2194 height: that.instance.container.height() + 'px', 2195 overflow: that.instance.container.css('overflow') 2196 }); 2197 that.instance.container.css({ 2198 overflow: 'hidden', 2199 position: 'absolute', 2200 top: '0px', 2201 left: '0px' 2202 }); 2203 that.instance.container.width(priv.virtualScroll.innerWidth() - this.scrollbarSize.width); 2204 that.instance.container.height(priv.virtualScroll.innerHeight() - this.scrollbarSize.height); 2205 setInterval(function () { 2206 priv.virtualScroll.find('.spacer').height(that.instance.table.height()); 2207 priv.virtualScroll.find('.spacer').width(that.instance.table.width()); 2208 }, 100); 2209 2210 that.scrollable.scrollTop(0); 2211 that.scrollable.scrollLeft(0); 2212 2213 that.scrollable.on('scroll.handsontable', function () { 2214 that.instance.curScrollTop = that.scrollable[0].scrollTop; 2215 that.instance.curScrollLeft = that.scrollable[0].scrollLeft; 2216 2217 if (that.instance.curScrollTop !== that.instance.lastScrollTop) { 2218 that.instance.blockedRows.refreshBorders(); 2219 that.instance.blockedCols.main[0].style.top = -that.instance.curScrollTop + 'px'; 2220 that.instance.table[0].style.top = -that.instance.curScrollTop + 'px'; 2221 } 2222 2223 if (that.instance.curScrollLeft !== that.instance.lastScrollLeft) { 2224 that.instance.blockedCols.refreshBorders(); 2225 that.instance.blockedRows.main[0].style.left = -that.instance.curScrollLeft + 'px'; 2226 that.instance.table[0].style.left = -that.instance.curScrollLeft + 'px'; 2227 } 2228 2229 if (that.instance.curScrollTop !== that.instance.lastScrollTop || that.instance.curScrollLeft !== that.instance.lastScrollLeft) { 2230 that.instance.selection.refreshBorders(); 2231 2232 if (that.instance.blockedCorner) { 2233 if (that.instance.curScrollTop === 0 && that.instance.curScrollLeft === 0) { 2234 that.instance.blockedCorner.find("th:last-child").css({borderRightWidth: 0}); 2235 that.instance.blockedCorner.find("tr:last-child th").css({borderBottomWidth: 0}); 2236 } 2237 else if (that.instance.lastScrollTop === 0 && that.instance.lastScrollLeft === 0) { 2238 that.instance.blockedCorner.find("th:last-child").css({borderRightWidth: '1px'}); 2239 that.instance.blockedCorner.find("tr:last-child th").css({borderBottomWidth: '1px'}); 2240 } 2241 } 2242 } 2243 2244 that.instance.lastScrollTop = that.instance.curScrollTop; 2245 that.instance.lastScrollLeft = that.instance.curScrollLeft; 2246 2247 that.instance.selection.refreshBorders(); 2248 }); 2249 2250 Handsontable.PluginHooks.push('afterInit', function () { 2251 that.scrollable.trigger('scroll.handsontable'); 2252 }); 2253 } 2254 else { 2255 that.scrollable = $(window); 2256 if (that.instance.blockedCorner) { 2257 that.instance.blockedCorner.find("th:last-child").css({borderRightWidth: 0}); 2258 that.instance.blockedCorner.find("tr:last-child th").css({borderBottomWidth: 0}); 2259 } 2260 } 2261 2262 that.scrollable.on('scroll', function (e) { 2263 e.stopPropagation(); 2264 }); 2265 2266 $(window).on('resize', function () { 2267 //https://github.com/warpech/jquery-handsontable/issues/193 2268 that.instance.blockedCols.update(); 2269 that.instance.blockedRows.update(); 2270 }); 2271 2272 $('.context-menu-root').on('mouseenter', onMouseEnterTable).on('mouseleave', onMouseLeaveTable); 2273 2274 }; 2275 2276 /** 2277 * Measure the width and height of browser scrollbar 2278 * @return {Object} 2279 */ 2280 Handsontable.TableView.prototype.measureScrollbar = function () { 2281 var div = $('<div style="width:150px;height:150px;overflow:hidden;position:absolute;top:200px;left:200px"><div style="width:100%;height:100%;position:absolute">x</div>'); 2282 $('body').append(div); 2283 var subDiv = $(div[0].firstChild); 2284 var w1 = subDiv.innerWidth(); 2285 var h1 = subDiv.innerHeight(); 2286 div[0].style.overflow = 'scroll'; 2287 w1 -= subDiv.innerWidth(); 2288 h1 -= subDiv.innerHeight(); 2289 if (w1 === 0) { 2290 w1 = 17; 2291 } 2292 if (h1 === 0) { 2293 h1 = 17; 2294 } 2295 div.remove(); 2296 return {width: w1, height: h1}; 2297 }; 2298 2299 /** 2300 * Creates row at the bottom of the <table> 2301 * @param {Object} [coords] Optional. Coords of the cell before which the new row will be inserted 2302 */ 2303 Handsontable.TableView.prototype.createRow = function (coords) { 2304 var tr, c, r, td, p; 2305 tr = document.createElement('tr'); 2306 this.instance.blockedCols.createRow(tr); 2307 for (c = 0; c < this.instance.colCount; c++) { 2308 tr.appendChild(td = document.createElement('td')); 2309 this.instance.minWidthFix(td); 2310 } 2311 if (!coords || coords.row >= this.instance.rowCount) { 2312 this.$tableBody.appendChild(tr); 2313 r = this.instance.rowCount; 2314 } 2315 else { 2316 var oldTr = this.instance.getCell(coords.row, coords.col).parentNode; 2317 this.$tableBody.insertBefore(tr, oldTr); 2318 r = coords.row; 2319 } 2320 this.instance.rowCount++; 2321 for (c = 0; c < this.instance.colCount; c++) { 2322 p = this.instance.colToProp(c); 2323 if(r == null || this.instance.getData()[r] == null || this.instance.getData()[r][p]) continue; 2324 2325 this.render(r, c, p, this.instance.getData()[r][p]); 2326 } 2327 }; 2328 2329 /** 2330 * Creates col at the right of the <table> 2331 * @param {Object} [coords] Optional. Coords of the cell before which the new column will be inserted 2332 */ 2333 Handsontable.TableView.prototype.createCol = function (coords) { 2334 var trs = this.$tableBody.childNodes, r, c, td, p; 2335 this.instance.blockedRows.createCol(); 2336 if (!coords || coords.col >= this.instance.colCount) { 2337 for (r = 0; r < this.instance.rowCount; r++) { 2338 trs[r].appendChild(td = document.createElement('td')); 2339 this.instance.minWidthFix(td); 2340 } 2341 c = this.instance.colCount; 2342 } 2343 else { 2344 for (r = 0; r < this.instance.rowCount; r++) { 2345 trs[r].insertBefore(td = document.createElement('td'), this.instance.getCell(r, coords.col)); 2346 this.instance.minWidthFix(td); 2347 } 2348 c = coords.col; 2349 } 2350 this.instance.colCount++; 2351 for (r = 0; r < this.instance.rowCount; r++) { 2352 p = this.instance.colToProp(c); 2353 this.render(r, c, p, this.instance.getData()[r][p]); 2354 } 2355 }; 2356 2357 /** 2358 * Removes row at the bottom of the <table> 2359 * @param {Object} [coords] Optional. Coords of the cell which row will be removed 2360 * @param {Object} [toCoords] Required if coords is defined. Coords of the cell until which all rows will be removed 2361 */ 2362 Handsontable.TableView.prototype.removeRow = function (coords, toCoords) { 2363 if (!coords || coords.row === this.instance.rowCount - 1) { 2364 $(this.$tableBody.childNodes[this.instance.rowCount - 1]).remove(); 2365 this.instance.rowCount--; 2366 } 2367 else { 2368 for (var i = toCoords.row; i >= coords.row; i--) { 2369 $(this.$tableBody.childNodes[i]).remove(); 2370 this.instance.rowCount--; 2371 } 2372 } 2373 }; 2374 2375 /** 2376 * Removes col at the right of the <table> 2377 * @param {Object} [coords] Optional. Coords of the cell which col will be removed 2378 * @param {Object} [toCoords] Required if coords is defined. Coords of the cell until which all cols will be removed 2379 */ 2380 Handsontable.TableView.prototype.removeCol = function (coords, toCoords) { 2381 var trs = this.$tableBody.childNodes, colThs, i; 2382 if (this.instance.blockedRows) { 2383 colThs = this.instance.table.find('thead th'); 2384 } 2385 var r = 0; 2386 if (!coords || coords.col === this.instance.colCount - 1) { 2387 for (; r < this.instance.rowCount; r++) { 2388 $(trs[r].childNodes[this.instance.colCount + this.instance.blockedCols.count() - 1]).remove(); 2389 if (colThs) { 2390 colThs.eq(this.instance.colCount + this.instance.blockedCols.count() - 1).remove(); 2391 } 2392 } 2393 this.instance.colCount--; 2394 } 2395 else { 2396 for (; r < this.instance.rowCount; r++) { 2397 for (i = toCoords.col; i >= coords.col; i--) { 2398 $(trs[r].childNodes[i + this.instance.blockedCols.count()]).remove(); 2399 2400 } 2401 } 2402 if (colThs) { 2403 for (i = toCoords.col; i >= coords.col; i--) { 2404 colThs.eq(i + this.instance.blockedCols.count()).remove(); 2405 } 2406 } 2407 this.instance.colCount -= toCoords.col - coords.col + 1; 2408 } 2409 }; 2410 2411 2412 Handsontable.TableView.prototype.render = function (row, col, prop, value) { 2413 var coords = {row: row, col: col}; 2414 var td = this.instance.getCell(row, col); 2415 this.applyCellTypeMethod('renderer', td, coords, value); 2416 this.instance.minWidthFix(td); 2417 return td; 2418 }; 2419 2420 2421 Handsontable.TableView.prototype.applyCellTypeMethod = function (methodName, td, coords, extraParam) { 2422 var prop = this.instance.colToProp(coords.col) 2423 , method 2424 , cellProperties = this.instance.getCellMeta(coords.row, coords.col) 2425 , settings = this.instance.getSettings(); 2426 2427 if (cellProperties.type && typeof cellProperties.type[methodName] === "function") { 2428 method = cellProperties.type[methodName]; 2429 } 2430 else if (settings.autoComplete) { 2431 for (var i = 0, ilen = settings.autoComplete.length; i < ilen; i++) { 2432 if (settings.autoComplete[i].match(coords.row, coords.col, this.instance.getData())) { 2433 method = Handsontable.AutocompleteCell[methodName]; 2434 cellProperties.autoComplete = settings.autoComplete[i]; 2435 break; 2436 } 2437 } 2438 } 2439 if (typeof method !== "function") { 2440 method = Handsontable.TextCell[methodName]; 2441 } 2442 return method(this.instance, td, coords.row, coords.col, prop, extraParam, cellProperties); 2443 }; 2444 2445 /** 2446 * Returns coordinates given td object 2447 */ 2448 Handsontable.TableView.prototype.getCellCoords = function (td) { 2449 return { 2450 row: td.parentNode.rowIndex - this.instance.blockedRows.count(), 2451 col: td.cellIndex - this.instance.blockedCols.count() 2452 }; 2453 }; 2454 2455 /** 2456 * Returns td object given coordinates 2457 */ 2458 Handsontable.TableView.prototype.getCellAtCoords = function (coords) { 2459 if (coords.row < 0 || coords.col < 0) { 2460 return null; 2461 } 2462 var tr = this.$tableBody.childNodes[coords.row]; 2463 if (tr) { 2464 return tr.childNodes[coords.col + this.instance.blockedCols.count()]; 2465 } 2466 else { 2467 return null; 2468 } 2469 }; 2470 2471 /** 2472 * Returns all td objects in grid 2473 */ 2474 Handsontable.TableView.prototype.getAllCells = function () { 2475 var tds = [], trs, r, rlen, c, clen; 2476 trs = this.$tableBody.childNodes; 2477 rlen = this.instance.rowCount; 2478 if (rlen > 0) { 2479 clen = this.instance.colCount; 2480 for (r = 0; r < rlen; r++) { 2481 for (c = 0; c < clen; c++) { 2482 tds.push(trs[r].childNodes[c + this.instance.blockedCols.count()]); 2483 } 2484 } 2485 } 2486 return tds; 2487 }; 2488 2489 /** 2490 * Scroll viewport to selection 2491 * @param td 2492 */ 2493 Handsontable.TableView.prototype.scrollViewport = function (td) { 2494 if (!this.instance.selection.isSelected()) { 2495 return false; 2496 } 2497 2498 var $td = $(td); 2499 var tdOffset = $td.offset(); 2500 var scrollLeft = this.scrollable.scrollLeft(); //scrollbar position 2501 var scrollTop = this.scrollable.scrollTop(); //scrollbar position 2502 var scrollOffset = this.scrollable.offset(); 2503 var rowHeaderWidth = this.instance.blockedCols.count() ? $(this.instance.blockedCols.main[0].firstChild).outerWidth() : 2; 2504 var colHeaderHeight = this.instance.blockedRows.count() ? $(this.instance.blockedRows.main[0].firstChild).outerHeight() : 2; 2505 2506 var offsetTop = tdOffset.top; 2507 var offsetLeft = tdOffset.left; 2508 var scrollWidth, scrollHeight; 2509 if (scrollOffset) { //if is not the window 2510 scrollWidth = this.scrollable.outerWidth(); 2511 scrollHeight = this.scrollable.outerHeight(); 2512 offsetTop += scrollTop - scrollOffset.top; 2513 offsetLeft += scrollLeft - scrollOffset.left; 2514 } 2515 else { 2516 scrollWidth = this.scrollable.width(); //don't use outerWidth with window (http://api.jquery.com/outerWidth/) 2517 scrollHeight = this.scrollable.height(); 2518 } 2519 scrollWidth -= this.scrollbarSize.width; 2520 scrollHeight -= this.scrollbarSize.height; 2521 2522 var height = $td.outerHeight(); 2523 var width = $td.outerWidth(); 2524 2525 var that = this; 2526 if (scrollLeft + scrollWidth <= offsetLeft + width) { 2527 setTimeout(function () { 2528 that.scrollable.scrollLeft(offsetLeft + width - scrollWidth); 2529 }, 1); 2530 } 2531 else if (scrollLeft > offsetLeft - rowHeaderWidth) { 2532 setTimeout(function () { 2533 that.scrollable.scrollLeft(offsetLeft - rowHeaderWidth); 2534 }, 1); 2535 } 2536 2537 if (scrollTop + scrollHeight <= offsetTop + height) { 2538 setTimeout(function () { 2539 that.scrollable.scrollTop(offsetTop + height - scrollHeight); 2540 }, 1); 2541 } 2542 else if (scrollTop > offsetTop - colHeaderHeight) { 2543 setTimeout(function () { 2544 that.scrollable.scrollTop(offsetTop - colHeaderHeight); 2545 }, 1); 2546 } 2547 }; 2548 /** 2549 * Returns true if keyCode represents a printable character 2550 * @param {Number} keyCode 2551 * @return {Boolean} 2552 */ 2553 Handsontable.helper.isPrintableChar = function (keyCode) { 2554 return ((keyCode == 32) || //space 2555 (keyCode >= 48 && keyCode <= 57) || //0-9 2556 (keyCode >= 96 && keyCode <= 111) || //numpad 2557 (keyCode >= 186 && keyCode <= 192) || //;=,-./` 2558 (keyCode >= 219 && keyCode <= 222) || //[]{}\|"' 2559 keyCode >= 226 || //special chars (229 for Asian chars) 2560 (keyCode >= 65 && keyCode <= 90)); //a-z 2561 }; 2562 2563 /** 2564 * Converts a value to string 2565 * @param value 2566 * @return {String} 2567 */ 2568 Handsontable.helper.stringify = function (value) { 2569 switch (typeof value) { 2570 case 'string': 2571 case 'number': 2572 return value + ''; 2573 break; 2574 2575 case 'object': 2576 if (value === null) { 2577 return ''; 2578 } 2579 else { 2580 return value.toString(); 2581 } 2582 break; 2583 2584 case 'undefined': 2585 return ''; 2586 break; 2587 2588 default: 2589 return value.toString(); 2590 } 2591 }; 2592 2593 /** 2594 * Create DOM elements for selection border lines (top, right, bottom, left) and optionally background 2595 * @constructor 2596 * @param {Object} instance Handsontable instance 2597 * @param {Object} options Configurable options 2598 * @param {Boolean} [options.bg] Should include a background 2599 * @param {String} [options.className] CSS class for border elements 2600 */ 2601 Handsontable.Border = function (instance, options) { 2602 this.instance = instance; 2603 this.$container = instance.container; 2604 var container = this.$container[0]; 2605 2606 if (options.bg) { 2607 this.bg = document.createElement("div"); 2608 this.bg.className = 'htBorderBg ' + options.className; 2609 container.insertBefore(this.bg, container.firstChild); 2610 } 2611 2612 this.main = document.createElement("div"); 2613 this.main.style.position = 'absolute'; 2614 this.main.style.top = 0; 2615 this.main.style.left = 0; 2616 this.main.innerHTML = (new Array(5)).join('<div class="htBorder ' + options.className + '"></div>'); 2617 this.disappear(); 2618 container.appendChild(this.main); 2619 2620 var nodes = this.main.childNodes; 2621 this.top = nodes[0]; 2622 this.left = nodes[1]; 2623 this.bottom = nodes[2]; 2624 this.right = nodes[3]; 2625 2626 this.borderWidth = $(this.left).width(); 2627 }; 2628 2629 Handsontable.Border.prototype = { 2630 /** 2631 * Show border around one or many cells 2632 * @param {Object[]} coordsArr 2633 */ 2634 appear: function (coordsArr) { 2635 var $from, $to, fromOffset, toOffset, containerOffset, top, minTop, left, minLeft, height, width; 2636 if (this.disabled) { 2637 return; 2638 } 2639 2640 this.corners = this.instance.getCornerCoords(coordsArr); 2641 2642 $from = $(this.instance.getCell(this.corners.TL.row, this.corners.TL.col)); 2643 $to = (coordsArr.length > 1) ? $(this.instance.getCell(this.corners.BR.row, this.corners.BR.col)) : $from; 2644 fromOffset = $from.offset(); 2645 toOffset = (coordsArr.length > 1) ? $to.offset() : fromOffset; 2646 containerOffset = this.$container.offset(); 2647 2648 minTop = fromOffset.top; 2649 height = toOffset.top + $to.outerHeight() - minTop; 2650 minLeft = fromOffset.left; 2651 width = toOffset.left + $to.outerWidth() - minLeft; 2652 2653 top = minTop - containerOffset.top + this.$container.scrollTop() - 1; 2654 left = minLeft - containerOffset.left + this.$container.scrollLeft() - 1; 2655 2656 if (parseInt($from.css('border-top-width')) > 0) { 2657 top += 1; 2658 height -= 1; 2659 } 2660 if (parseInt($from.css('border-left-width')) > 0) { 2661 left += 1; 2662 width -= 1; 2663 } 2664 2665 if (this.bg) { 2666 this.bg.style.top = top + 'px'; 2667 this.bg.style.left = left + 'px'; 2668 this.bg.style.width = width + 'px'; 2669 this.bg.style.height = height + 'px'; 2670 this.bg.style.display = 'block'; 2671 } 2672 2673 this.top.style.top = top + 'px'; 2674 this.top.style.left = left + 'px'; 2675 this.top.style.width = width + 'px'; 2676 2677 this.left.style.top = top + 'px'; 2678 this.left.style.left = left + 'px'; 2679 this.left.style.height = height + 'px'; 2680 2681 var delta = Math.floor(this.borderWidth / 2); 2682 2683 this.bottom.style.top = top + height - delta + 'px'; 2684 this.bottom.style.left = left + 'px'; 2685 this.bottom.style.width = width + 'px'; 2686 2687 this.right.style.top = top + 'px'; 2688 this.right.style.left = left + width - delta + 'px'; 2689 this.right.style.height = height + 1 + 'px'; 2690 2691 this.main.style.display = 'block'; 2692 }, 2693 2694 /** 2695 * Hide border 2696 */ 2697 disappear: function () { 2698 this.main.style.display = 'none'; 2699 if (this.bg) { 2700 this.bg.style.display = 'none'; 2701 } 2702 this.corners = null; 2703 } 2704 }; 2705 /** 2706 * Create DOM element for drag-down handle 2707 * @constructor 2708 * @param {Object} instance Handsontable instance 2709 */ 2710 Handsontable.FillHandle = function (instance) { 2711 this.instance = instance; 2712 this.$container = instance.container; 2713 var container = this.$container[0]; 2714 2715 this.handle = document.createElement("div"); 2716 this.handle.className = "htFillHandle"; 2717 this.disappear(); 2718 container.appendChild(this.handle); 2719 2720 var that = this; 2721 $(this.handle).mousedown(function () { 2722 that.isDragged = 1; 2723 }); 2724 2725 this.$container.find('table').on('selectstart', function (event) { 2726 //https://github.com/warpech/jquery-handsontable/issues/160 2727 //selectstart is IE only event. Prevent text from being selected when performing drag down in IE8 2728 event.preventDefault(); 2729 }); 2730 }; 2731 2732 Handsontable.FillHandle.prototype = { 2733 /** 2734 * Show handle in cell cornerÅ‚ 2735 * @param {Object[]} coordsArr 2736 */ 2737 appear: function (coordsArr) { 2738 if (this.disabled) { 2739 return; 2740 } 2741 2742 var $td, tdOffset, containerOffset, top, left, height, width; 2743 2744 var corners = this.instance.getCornerCoords(coordsArr); 2745 2746 $td = $(this.instance.getCell(corners.BR.row, corners.BR.col)); 2747 tdOffset = $td.offset(); 2748 containerOffset = this.$container.offset(); 2749 2750 top = tdOffset.top - containerOffset.top + this.$container.scrollTop() - 1; 2751 left = tdOffset.left - containerOffset.left + this.$container.scrollLeft() - 1; 2752 height = $td.outerHeight(); 2753 width = $td.outerWidth(); 2754 2755 this.handle.style.top = top + height - 3 + 'px'; 2756 this.handle.style.left = left + width - 3 + 'px'; 2757 this.handle.style.display = 'block'; 2758 }, 2759 2760 /** 2761 * Hide handle 2762 */ 2763 disappear: function () { 2764 this.handle.style.display = 'none'; 2765 } 2766 }; 2767 /** 2768 * Handsontable UndoRedo class 2769 */ 2770 Handsontable.UndoRedo = function (instance) { 2771 var that = this; 2772 this.instance = instance; 2773 this.clear(); 2774 instance.rootElement.on("datachange.handsontable", function (event, changes, origin) { 2775 if (origin !== 'undo' && origin !== 'redo') { 2776 that.add(changes); 2777 } 2778 }); 2779 }; 2780 2781 /** 2782 * Undo operation from current revision 2783 */ 2784 Handsontable.UndoRedo.prototype.undo = function () { 2785 var i, ilen; 2786 if (this.isUndoAvailable()) { 2787 var setData = $.extend(true, [], this.data[this.rev]); 2788 for (i = 0, ilen = setData.length; i < ilen; i++) { 2789 setData[i].splice(3, 1); 2790 } 2791 this.instance.setDataAtCell(setData, null, null, 'undo'); 2792 this.rev--; 2793 } 2794 }; 2795 2796 /** 2797 * Redo operation from current revision 2798 */ 2799 Handsontable.UndoRedo.prototype.redo = function () { 2800 var i, ilen; 2801 if (this.isRedoAvailable()) { 2802 this.rev++; 2803 var setData = $.extend(true, [], this.data[this.rev]); 2804 for (i = 0, ilen = setData.length; i < ilen; i++) { 2805 setData[i].splice(2, 1); 2806 } 2807 this.instance.setDataAtCell(setData, null, null, 'redo'); 2808 } 2809 }; 2810 2811 /** 2812 * Returns true if undo point is available 2813 * @return {Boolean} 2814 */ 2815 Handsontable.UndoRedo.prototype.isUndoAvailable = function () { 2816 return (this.rev >= 0); 2817 }; 2818 2819 /** 2820 * Returns true if redo point is available 2821 * @return {Boolean} 2822 */ 2823 Handsontable.UndoRedo.prototype.isRedoAvailable = function () { 2824 return (this.rev < this.data.length - 1); 2825 }; 2826 2827 /** 2828 * Add new history poins 2829 * @param changes 2830 */ 2831 Handsontable.UndoRedo.prototype.add = function (changes) { 2832 this.rev++; 2833 this.data.splice(this.rev); //if we are in point abcdef(g)hijk in history, remove everything after (g) 2834 this.data.push(changes); 2835 }; 2836 2837 /** 2838 * Clears undo history 2839 */ 2840 Handsontable.UndoRedo.prototype.clear = function () { 2841 this.data = []; 2842 this.rev = -1; 2843 }; 2844 /** 2845 * Handsontable BlockedRows class 2846 * @param {Object} instance 2847 */ 2848 Handsontable.BlockedRows = function (instance) { 2849 var that = this; 2850 this.instance = instance; 2851 this.headers = []; 2852 var position = instance.table.position(); 2853 instance.positionFix(position); 2854 this.main = $('<div style="position: absolute; top: ' + position.top + 'px; left: ' + position.left + 'px"><table class="htBlockedRows" cellspacing="0" cellpadding="0"><thead></thead></table></div>'); 2855 this.instance.container.append(this.main); 2856 this.hasCSS3 = !($.browser.msie && (parseInt($.browser.version, 10) <= 8)); //Used to get over IE8- not having :last-child selector 2857 this.update(); 2858 this.instance.rootElement.on('cellrender.handsontable', function (event, changes, source) { 2859 setTimeout(function () { 2860 that.dimensions(); 2861 }, 10); 2862 }); 2863 }; 2864 2865 /** 2866 * Returns number of blocked cols 2867 */ 2868 Handsontable.BlockedRows.prototype.count = function () { 2869 return this.headers.length; 2870 }; 2871 2872 /** 2873 * Create column header in the grid table 2874 */ 2875 Handsontable.BlockedRows.prototype.createCol = function (className) { 2876 var $tr, th, h, hlen = this.count(); 2877 for (h = 0; h < hlen; h++) { 2878 $tr = this.main.find('thead tr.' + this.headers[h].className); 2879 if (!$tr.length) { 2880 $tr = $('<tr class="' + this.headers[h].className + '"></tr>'); 2881 this.main.find('thead').append($tr); 2882 } 2883 $tr = this.instance.table.find('thead tr.' + this.headers[h].className); 2884 if (!$tr.length) { 2885 $tr = $('<tr class="' + this.headers[h].className + '"></tr>'); 2886 this.instance.table.find('thead').append($tr); 2887 } 2888 2889 th = document.createElement('th'); 2890 th.className = this.headers[h].className; 2891 if (className) { 2892 th.className += ' ' + className; 2893 } 2894 th.innerHTML = this.headerText(' '); 2895 this.instance.minWidthFix(th); 2896 this.instance.table.find('thead tr.' + this.headers[h].className)[0].appendChild(th); 2897 2898 th = document.createElement('th'); 2899 th.className = this.headers[h].className; 2900 if (className) { 2901 th.className += ' ' + className; 2902 } 2903 this.instance.minWidthFix(th); 2904 this.main.find('thead tr.' + this.headers[h].className)[0].appendChild(th); 2905 } 2906 }; 2907 2908 /** 2909 * Create column header in the grid table 2910 */ 2911 Handsontable.BlockedRows.prototype.create = function () { 2912 var c; 2913 if (this.count() > 0) { 2914 this.instance.table.find('thead').empty(); 2915 this.main.find('thead').empty(); 2916 var offset = this.instance.blockedCols.count(); 2917 for (c = offset - 1; c >= 0; c--) { 2918 this.createCol(this.instance.blockedCols.headers[c].className); 2919 } 2920 for (c = 0; c < this.instance.colCount; c++) { 2921 this.createCol(); 2922 } 2923 } 2924 if (!this.hasCSS3) { 2925 this.instance.container.find('thead tr.lastChild').not(':last-child').removeClass('lastChild'); 2926 this.instance.container.find('thead tr:last-child').not('.lastChild').addClass('lastChild'); 2927 } 2928 }; 2929 2930 /** 2931 * Copy table column header onto the floating layer above the grid 2932 */ 2933 Handsontable.BlockedRows.prototype.refresh = function () { 2934 var label; 2935 if (this.count() > 0) { 2936 var that = this; 2937 var hlen = this.count(), h; 2938 for (h = 0; h < hlen; h++) { 2939 var $tr = this.main.find('thead tr.' + this.headers[h].className); 2940 var tr = $tr[0]; 2941 var ths = tr.childNodes; 2942 var thsLen = ths.length; 2943 var offset = this.instance.blockedCols.count(); 2944 2945 while (thsLen > this.instance.colCount + offset) { 2946 //remove excessive cols 2947 thsLen--; 2948 $(tr.childNodes[thsLen]).remove(); 2949 } 2950 2951 for (h = 0; h < hlen; h++) { 2952 var realThs = this.instance.table.find('thead th.' + this.headers[h].className); 2953 for (var i = 0; i < thsLen; i++) { 2954 label = that.headers[h].columnLabel(i - offset); 2955 if (this.headers[h].format && this.headers[h].format === 'small') { 2956 realThs[i].innerHTML = this.headerText(label); 2957 ths[i].innerHTML = this.headerText(label); 2958 } 2959 else { 2960 realThs[i].innerHTML = label; 2961 ths[i].innerHTML = label; 2962 } 2963 this.instance.minWidthFix(realThs[i]); 2964 this.instance.minWidthFix(ths[i]); 2965 ths[i].style.minWidth = realThs.eq(i).width() + 'px'; 2966 } 2967 } 2968 } 2969 2970 this.ths = this.main.find('tr:last-child th'); 2971 this.refreshBorders(); 2972 } 2973 }; 2974 2975 /** 2976 * Refresh border width 2977 */ 2978 Handsontable.BlockedRows.prototype.refreshBorders = function () { 2979 if (this.count() > 0) { 2980 if (this.instance.curScrollTop === 0) { 2981 this.ths.css('borderBottomWidth', 0); 2982 } 2983 else if (this.instance.lastScrollTop === 0) { 2984 this.ths.css('borderBottomWidth', '1px'); 2985 } 2986 } 2987 }; 2988 2989 /** 2990 * Recalculate column widths on the floating layer above the grid 2991 */ 2992 Handsontable.BlockedRows.prototype.dimensions = function () { 2993 if (this.count() > 0) { 2994 var realThs = this.instance.table.find('thead th'); 2995 for (var i = 0, ilen = realThs.length; i < ilen; i++) { 2996 this.ths[i].style.minWidth = $(realThs[i]).width() + 'px'; 2997 } 2998 } 2999 }; 3000 3001 3002 /** 3003 * Update settings of the column header 3004 */ 3005 Handsontable.BlockedRows.prototype.update = function () { 3006 this.create(); 3007 this.refresh(); 3008 }; 3009 3010 /** 3011 * Add column header to DOM 3012 */ 3013 Handsontable.BlockedRows.prototype.addHeader = function (header) { 3014 for (var h = this.count() - 1; h >= 0; h--) { 3015 if (this.headers[h].className === header.className) { 3016 this.headers.splice(h, 1); //if exists, remove then add to recreate 3017 } 3018 } 3019 this.headers.push(header); 3020 this.headers.sort(function (a, b) { 3021 return a.priority || 0 - b.priority || 0 3022 }); 3023 this.update(); 3024 }; 3025 3026 /** 3027 * Remove column header from DOM 3028 */ 3029 Handsontable.BlockedRows.prototype.destroyHeader = function (className) { 3030 for (var h = this.count() - 1; h >= 0; h--) { 3031 if (this.headers[h].className === className) { 3032 this.main.find('thead tr.' + this.headers[h].className).remove(); 3033 this.instance.table.find('thead tr.' + this.headers[h].className).remove(); 3034 this.headers.splice(h, 1); 3035 } 3036 } 3037 }; 3038 3039 /** 3040 * Puts string to small text template 3041 */ 3042 Handsontable.BlockedRows.prototype.headerText = function (str) { 3043 return ' <span class="small">' + str + '</span> '; 3044 }; 3045 /** 3046 * Handsontable BlockedCols class 3047 * @param {Object} instance 3048 */ 3049 Handsontable.BlockedCols = function (instance) { 3050 var that = this; 3051 this.instance = instance; 3052 this.headers = []; 3053 var position = instance.table.position(); 3054 instance.positionFix(position); 3055 this.main = $('<div style="position: absolute; top: ' + position.top + 'px; left: ' + position.left + 'px"><table class="htBlockedCols" cellspacing="0" cellpadding="0"><thead><tr></tr></thead><tbody></tbody></table></div>'); 3056 this.instance.container.append(this.main); 3057 this.heightMethod = this.determineCellHeightMethod(); 3058 this.instance.rootElement.on('cellrender.handsontable', function (/*event, changes, source*/) { 3059 setTimeout(function () { 3060 that.dimensions(); 3061 }, 10); 3062 }); 3063 }; 3064 3065 /** 3066 * Determine cell height method 3067 * @return {String} 3068 */ 3069 Handsontable.BlockedCols.prototype.determineCellHeightMethod = function () { 3070 return 'height'; 3071 }; 3072 3073 /** 3074 * Returns number of blocked cols 3075 */ 3076 Handsontable.BlockedCols.prototype.count = function () { 3077 return this.headers.length; 3078 }; 3079 3080 /** 3081 * Create row header in the grid table 3082 */ 3083 Handsontable.BlockedCols.prototype.createRow = function (tr) { 3084 var th; 3085 var mainTr = document.createElement('tr'); 3086 3087 for (var h = 0, hlen = this.count(); h < hlen; h++) { 3088 th = document.createElement('th'); 3089 th.className = this.headers[h].className; 3090 this.instance.minWidthFix(th); 3091 tr.insertBefore(th, tr.firstChild); 3092 3093 th = document.createElement('th'); 3094 th.className = this.headers[h].className; 3095 mainTr.insertBefore(th, mainTr.firstChild); 3096 } 3097 3098 this.main.find('tbody')[0].appendChild(mainTr); 3099 }; 3100 3101 /** 3102 * Create row header in the grid table 3103 */ 3104 Handsontable.BlockedCols.prototype.create = function () { 3105 var hlen = this.count(), h, th; 3106 this.main.find('tbody').empty(); 3107 this.instance.table.find('tbody th').remove(); 3108 var $theadTr = this.main.find('thead tr'); 3109 $theadTr.empty(); 3110 3111 if (hlen > 0) { 3112 var offset = this.instance.blockedRows.count(); 3113 if (offset) { 3114 for (h = 0; h < hlen; h++) { 3115 th = $theadTr[0].getElementsByClassName ? $theadTr[0].getElementsByClassName(this.headers[h].className)[0] : $theadTr.find('.' + this.headers[h].className.replace(/\s/i, '.'))[0]; 3116 if (!th) { 3117 th = document.createElement('th'); 3118 th.className = this.headers[h].className; 3119 th.innerHTML = this.headerText(' '); 3120 this.instance.minWidthFix(th); 3121 $theadTr[0].insertBefore(th, $theadTr[0].firstChild); 3122 } 3123 } 3124 } 3125 3126 var trs = this.instance.table.find('tbody')[0].childNodes; 3127 for (var r = 0; r < this.instance.rowCount; r++) { 3128 this.createRow(trs[r]); 3129 } 3130 } 3131 }; 3132 3133 /** 3134 * Copy table row header onto the floating layer above the grid 3135 */ 3136 Handsontable.BlockedCols.prototype.refresh = function () { 3137 var hlen = this.count(), h, th, realTh, i, label; 3138 if (hlen > 0) { 3139 var $tbody = this.main.find('tbody'); 3140 var tbody = $tbody[0]; 3141 var trs = tbody.childNodes; 3142 var trsLen = trs.length; 3143 while (trsLen > this.instance.rowCount) { 3144 //remove excessive rows 3145 trsLen--; 3146 $(tbody.childNodes[trsLen]).remove(); 3147 } 3148 3149 var realTrs = this.instance.table.find('tbody tr'); 3150 for (i = 0; i < trsLen; i++) { 3151 for (h = 0; h < hlen; h++) { 3152 label = this.headers[h].columnLabel(i); 3153 realTh = realTrs[i].getElementsByClassName ? realTrs[i].getElementsByClassName(this.headers[h].className)[0] : $(realTrs[i]).find('.' + this.headers[h].className.replace(/\s/i, '.'))[0]; 3154 th = trs[i].getElementsByClassName ? trs[i].getElementsByClassName(this.headers[h].className)[0] : $(trs[i]).find('.' + this.headers[h].className.replace(/\s/i, '.'))[0]; 3155 if (this.headers[h].format && this.headers[h].format === 'small') { 3156 realTh.innerHTML = this.headerText(label); 3157 th.innerHTML = this.headerText(label); 3158 } 3159 else { 3160 realTh.innerHTML = label; 3161 th.innerHTML = label; 3162 } 3163 this.instance.minWidthFix(th); 3164 th.style.height = $(realTh)[this.heightMethod]() + 'px'; 3165 } 3166 } 3167 3168 this.ths = this.main.find('th:last-child'); 3169 this.refreshBorders(); 3170 } 3171 }; 3172 3173 /** 3174 * Refresh border width 3175 */ 3176 Handsontable.BlockedCols.prototype.refreshBorders = function () { 3177 if (this.count() > 0) { 3178 if (this.instance.curScrollLeft === 0) { 3179 this.ths.css('borderRightWidth', 0); 3180 } 3181 else if (this.instance.lastScrollLeft === 0) { 3182 this.ths.css('borderRightWidth', '1px'); 3183 } 3184 } 3185 }; 3186 3187 /** 3188 * Recalculate row heights on the floating layer above the grid 3189 */ 3190 Handsontable.BlockedCols.prototype.dimensions = function () { 3191 if (this.count() > 0) { 3192 var realTrs = this.instance.table[0].getElementsByTagName('tbody')[0].childNodes; 3193 var trs = this.main[0].firstChild.getElementsByTagName('tbody')[0].childNodes; 3194 for (var i = 0, ilen = realTrs.length; i < ilen; i++) { 3195 trs[i].firstChild.style.height = $(realTrs[i].firstChild)[this.heightMethod]() + 'px'; 3196 } 3197 } 3198 }; 3199 3200 /** 3201 * Update settings of the row header 3202 */ 3203 Handsontable.BlockedCols.prototype.update = Handsontable.BlockedRows.prototype.update; 3204 3205 /** 3206 * Add row header to DOM 3207 */ 3208 Handsontable.BlockedCols.prototype.addHeader = function (header) { 3209 for (var h = this.count() - 1; h >= 0; h--) { 3210 if (this.headers[h].className === header.className) { 3211 this.headers.splice(h, 1); //if exists, remove then add to recreate 3212 } 3213 } 3214 this.headers.push(header); 3215 this.headers.sort(function (a, b) { 3216 return a.priority || 0 - b.priority || 0 3217 }); 3218 }; 3219 3220 /** 3221 * Remove row header from DOM 3222 */ 3223 Handsontable.BlockedCols.prototype.destroyHeader = function (className) { 3224 for (var h = this.count() - 1; h >= 0; h--) { 3225 if (this.headers[h].className === className) { 3226 this.headers.splice(h, 1); 3227 } 3228 } 3229 }; 3230 3231 /** 3232 * Puts string to small text template 3233 */ 3234 Handsontable.BlockedCols.prototype.headerText = Handsontable.BlockedRows.prototype.headerText; 3235 /** 3236 * Handsontable RowHeader extension 3237 * @param {Object} instance 3238 * @param {Array|Boolean} [labels] 3239 */ 3240 Handsontable.RowHeader = function (instance, labels) { 3241 var that = this; 3242 this.className = 'htRowHeader'; 3243 instance.blockedCols.main.on('mousedown', 'th.htRowHeader', function (event) { 3244 if (!$(event.target).hasClass('btn') && !$(event.target).hasClass('btnContainer')) { 3245 instance.deselectCell(); 3246 $(this).addClass('active'); 3247 that.lastActive = this; 3248 var offset = instance.blockedRows.count(); 3249 instance.selectCell(this.parentNode.rowIndex - offset, 0, this.parentNode.rowIndex - offset, instance.colCount - 1, false); 3250 } 3251 }); 3252 instance.rootElement.on('deselect.handsontable', function () { 3253 that.deselect(); 3254 }); 3255 this.labels = labels; 3256 this.instance = instance; 3257 this.instance.rowHeader = this; 3258 this.format = 'small'; 3259 instance.blockedCols.addHeader(this); 3260 }; 3261 3262 /** 3263 * Return custom row label or automatically generate one 3264 * @param {Number} index Row index 3265 * @return {String} 3266 */ 3267 Handsontable.RowHeader.prototype.columnLabel = function (index) { 3268 if (typeof this.labels[index] !== 'undefined') { 3269 return this.labels[index]; 3270 } 3271 return index + 1; 3272 }; 3273 3274 /** 3275 * Remove current highlight of a currently selected row header 3276 */ 3277 Handsontable.RowHeader.prototype.deselect = function () { 3278 if (this.lastActive) { 3279 $(this.lastActive).removeClass('active'); 3280 this.lastActive = null; 3281 } 3282 }; 3283 3284 /** 3285 * 3286 */ 3287 Handsontable.RowHeader.prototype.destroy = function () { 3288 this.instance.blockedCols.destroyHeader(this.className); 3289 }; 3290 /** 3291 * Handsontable ColHeader extension 3292 * @param {Object} instance 3293 * @param {Array|Boolean} [labels] 3294 */ 3295 Handsontable.ColHeader = function (instance, labels) { 3296 var that = this; 3297 this.className = 'htColHeader'; 3298 instance.blockedRows.main.on('mousedown', 'th.htColHeader', function () { 3299 instance.deselectCell(); 3300 var $th = $(this); 3301 $th.addClass('active'); 3302 that.lastActive = this; 3303 var index = $th.index(); 3304 var offset = instance.blockedCols ? instance.blockedCols.count() : 0; 3305 instance.selectCell(0, index - offset, instance.countRows() - 1, index - offset, false); 3306 }); 3307 instance.rootElement.on('deselect.handsontable', function () { 3308 that.deselect(); 3309 }); 3310 this.instance = instance; 3311 this.labels = labels; 3312 this.instance.colHeader = this; 3313 this.format = 'small'; 3314 instance.blockedRows.addHeader(this); 3315 }; 3316 3317 /** 3318 * Return custom column label or automatically generate one 3319 * @param {Number} index Row index 3320 * @return {String} 3321 */ 3322 Handsontable.ColHeader.prototype.columnLabel = function (index) { 3323 if (typeof this.labels[index] !== 'undefined') { 3324 return this.labels[index]; 3325 } 3326 var dividend = index + 1; 3327 var columnLabel = ''; 3328 var modulo; 3329 while (dividend > 0) { 3330 modulo = (dividend - 1) % 26; 3331 columnLabel = String.fromCharCode(65 + modulo) + columnLabel; 3332 dividend = parseInt((dividend - modulo) / 26); 3333 } 3334 return columnLabel; 3335 }; 3336 3337 /** 3338 * Remove current highlight of a currently selected column header 3339 */ 3340 Handsontable.ColHeader.prototype.deselect = Handsontable.RowHeader.prototype.deselect; 3341 3342 /** 3343 * 3344 */ 3345 Handsontable.ColHeader.prototype.destroy = function () { 3346 this.instance.blockedRows.destroyHeader(this.className); 3347 }; 3348 /** 3349 * Default text renderer 3350 * @param {Object} instance Handsontable instance 3351 * @param {Element} td Table cell where to render 3352 * @param {Number} row 3353 * @param {Number} col 3354 * @param {String|Number} prop Row object property name 3355 * @param value Value to render (remember to escape unsafe HTML before inserting to DOM!) 3356 * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) 3357 */ 3358 Handsontable.TextRenderer = function (instance, td, row, col, prop, value, cellProperties) { 3359 var escaped = Handsontable.helper.stringify(value); 3360 escaped = escaped.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); //escape html special chars 3361 td.innerHTML = escaped.replace(/\n/g, '<br/>'); 3362 }; 3363 /** 3364 * Autocomplete renderer 3365 * @param {Object} instance Handsontable instance 3366 * @param {Element} td Table cell where to render 3367 * @param {Number} row 3368 * @param {Number} col 3369 * @param {String|Number} prop Row object property name 3370 * @param value Value to render (remember to escape unsafe HTML before inserting to DOM!) 3371 * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) 3372 */ 3373 Handsontable.AutocompleteRenderer = function (instance, td, row, col, prop, value, cellProperties) { 3374 var $td = $(td); 3375 var $text = $('<div class="htAutocomplete"></div>'); 3376 var $arrow = $('<div class="htAutocompleteArrow">▼</div>'); 3377 $arrow.mouseup(function(){ 3378 $td.triggerHandler('dblclick.editor'); 3379 }); 3380 3381 Handsontable.TextCell.renderer(instance, $text[0], row, col, prop, value, cellProperties); 3382 3383 if($text.html() === '') { 3384 $text.html(' '); 3385 } 3386 3387 $text.append($arrow); 3388 $td.empty().append($text); 3389 }; 3390 /** 3391 * Checkbox renderer 3392 * @param {Object} instance Handsontable instance 3393 * @param {Element} td Table cell where to render 3394 * @param {Number} row 3395 * @param {Number} col 3396 * @param {String|Number} prop Row object property name 3397 * @param value Value to render (remember to escape unsafe HTML before inserting to DOM!) 3398 * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) 3399 */ 3400 Handsontable.CheckboxRenderer = function (instance, td, row, col, prop, value, cellProperties) { 3401 if (typeof cellProperties.checkedTemplate === "undefined") { 3402 cellProperties.checkedTemplate = true; 3403 } 3404 if (typeof cellProperties.uncheckedTemplate === "undefined") { 3405 cellProperties.uncheckedTemplate = false; 3406 } 3407 if (value === cellProperties.checkedTemplate || value === Handsontable.helper.stringify(cellProperties.checkedTemplate)) { 3408 td.innerHTML = "<input type='checkbox' checked autocomplete='no'>"; 3409 } 3410 else if (value === cellProperties.uncheckedTemplate || value === Handsontable.helper.stringify(cellProperties.uncheckedTemplate)) { 3411 td.innerHTML = "<input type='checkbox' autocomplete='no'>"; 3412 } 3413 else if (value === null) { //default value 3414 td.innerHTML = "<input type='checkbox' autocomplete='no' style='opacity: 0.5'>"; 3415 } 3416 else { 3417 td.innerHTML = "#bad value#"; 3418 } 3419 3420 $(td).find('input').change(function () { 3421 if ($(this).is(':checked')) { 3422 instance.setDataAtCell(row, prop, cellProperties.checkedTemplate); 3423 } 3424 else { 3425 instance.setDataAtCell(row, prop, cellProperties.uncheckedTemplate); 3426 } 3427 }); 3428 3429 return td; 3430 }; 3431 var texteditor = { 3432 isCellEdited: false, 3433 3434 /** 3435 * Returns caret position in edit proxy 3436 * @author http://stackoverflow.com/questions/263743/how-to-get-caret-position-in-textarea 3437 * @return {Number} 3438 */ 3439 getCaretPosition: function (keyboardProxy) { 3440 var el = keyboardProxy[0]; 3441 if (el.selectionStart) { 3442 return el.selectionStart; 3443 } 3444 else if (document.selection) { 3445 el.focus(); 3446 var r = document.selection.createRange(); 3447 if (r == null) { 3448 return 0; 3449 } 3450 var re = el.createTextRange(), 3451 rc = re.duplicate(); 3452 re.moveToBookmark(r.getBookmark()); 3453 rc.setEndPoint('EndToStart', re); 3454 return rc.text.length; 3455 } 3456 return 0; 3457 }, 3458 3459 /** 3460 * Sets caret position in edit proxy 3461 * @author http://blog.vishalon.net/index.php/javascript-getting-and-setting-caret-position-in-textarea/ 3462 * @param {Number} 3463 */ 3464 setCaretPosition: function (keyboardProxy, pos) { 3465 var el = keyboardProxy[0]; 3466 if (el.setSelectionRange) { 3467 el.focus(); 3468 el.setSelectionRange(pos, pos); 3469 } 3470 else if (el.createTextRange) { 3471 var range = el.createTextRange(); 3472 range.collapse(true); 3473 range.moveEnd('character', pos); 3474 range.moveStart('character', pos); 3475 range.select(); 3476 } 3477 }, 3478 3479 /** 3480 * Shows text input in grid cell 3481 */ 3482 beginEditing: function (instance, td, row, col, prop, keyboardProxy, useOriginalValue, suffix) { 3483 if (texteditor.isCellEdited) { 3484 return; 3485 } 3486 3487 keyboardProxy.on('cut.editor', function (event) { 3488 event.stopPropagation(); 3489 }); 3490 3491 keyboardProxy.on('paste.editor', function (event) { 3492 event.stopPropagation(); 3493 }); 3494 3495 var $td = $(td); 3496 3497 if (!instance.getCellMeta(row, col).isWritable) { 3498 return; 3499 } 3500 3501 texteditor.isCellEdited = true; 3502 3503 if (useOriginalValue) { 3504 var original = instance.getDataAtCell(row, prop); 3505 original = Handsontable.helper.stringify(original) + (suffix || ''); 3506 keyboardProxy.val(original); 3507 texteditor.setCaretPosition(keyboardProxy, original.length); 3508 } 3509 else { 3510 keyboardProxy.val(''); 3511 } 3512 3513 var width = $td.width() 3514 , height = $td.outerHeight() - 4; 3515 3516 if (parseInt($td.css('border-top-width')) > 0) { 3517 height -= 1; 3518 } 3519 if (parseInt($td.css('border-left-width')) > 0) { 3520 if (instance.blockedCols.count() > 0) { 3521 width -= 1; 3522 } 3523 } 3524 3525 keyboardProxy.autoResize({ 3526 maxHeight: 200, 3527 minHeight: height, 3528 minWidth: width, 3529 maxWidth: Math.max(168, width), 3530 animate: false, 3531 extraSpace: 0 3532 }); 3533 keyboardProxy.parent().removeClass('htHidden'); 3534 3535 instance.rootElement.triggerHandler('beginediting.handsontable'); 3536 3537 setTimeout(function () { 3538 //async fix for Firefox 3.6.28 (needs manual testing) 3539 keyboardProxy.parent().css({ 3540 overflow: 'visible' 3541 }); 3542 }, 1); 3543 }, 3544 3545 /** 3546 * Finishes text input in selected cells 3547 */ 3548 finishEditing: function (instance, td, row, col, prop, keyboardProxy, isCancelled, ctrlDown) { 3549 if (texteditor.isCellEdited) { 3550 texteditor.isCellEdited = false; 3551 var val = [ 3552 [$.trim(keyboardProxy.val())] 3553 ]; 3554 if (!isCancelled) { 3555 if (ctrlDown) { //if ctrl+enter and multiple cells selected, behave like Excel (finish editing and apply to all cells) 3556 var sel = instance.handsontable('getSelected'); 3557 instance.populateFromArray({row: sel[0], col: sel[1]}, val, {row: sel[2], col: sel[3]}, false, 'edit'); 3558 } 3559 else { 3560 instance.populateFromArray({row: row, col: col}, val, null, false, 'edit'); 3561 } 3562 keyboardProxy.off(".editor"); 3563 $(td).off('.editor'); 3564 } 3565 } 3566 else { 3567 keyboardProxy.off(".editor"); 3568 $(td).off('.editor'); 3569 } 3570 3571 keyboardProxy.css({ 3572 width: 0, 3573 height: 0 3574 }); 3575 keyboardProxy.parent().addClass('htHidden').css({ 3576 overflow: 'hidden' 3577 }); 3578 3579 instance.container.find('.htBorder.current').off('.editor'); 3580 instance.rootElement.triggerHandler('finishediting.handsontable'); 3581 } 3582 }; 3583 3584 /** 3585 * Default text editor 3586 * @param {Object} instance Handsontable instance 3587 * @param {Element} td Table cell where to render 3588 * @param {Number} row 3589 * @param {Number} col 3590 * @param {String|Number} prop Row object property name 3591 * @param {Object} keyboardProxy jQuery element of keyboard proxy that contains current editing value 3592 * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) 3593 */ 3594 Handsontable.TextEditor = function (instance, td, row, col, prop, keyboardProxy, cellProperties) { 3595 texteditor.isCellEdited = false; 3596 3597 var $current = $(td); 3598 var currentOffset = $current.offset(); 3599 var containerOffset = instance.container.offset(); 3600 var scrollTop = instance.container.scrollTop(); 3601 var scrollLeft = instance.container.scrollLeft(); 3602 var editTop = currentOffset.top - containerOffset.top + scrollTop - 1; 3603 var editLeft = currentOffset.left - containerOffset.left + scrollLeft - 1; 3604 3605 if (editTop < 0) { 3606 editTop = 0; 3607 } 3608 if (editLeft < 0) { 3609 editLeft = 0; 3610 } 3611 3612 if (instance.blockedRows.count() > 0 && parseInt($current.css('border-top-width')) > 0) { 3613 editTop += 1; 3614 } 3615 if (instance.blockedCols.count() > 0 && parseInt($current.css('border-left-width')) > 0) { 3616 editLeft += 1; 3617 } 3618 3619 if ($.browser.msie && parseInt($.browser.version, 10) <= 7) { 3620 editTop -= 1; 3621 } 3622 3623 keyboardProxy.parent().addClass('htHidden').css({ 3624 top: editTop, 3625 left: editLeft, 3626 overflow: 'hidden' 3627 }); 3628 keyboardProxy.css({ 3629 width: 0, 3630 height: 0 3631 }); 3632 3633 keyboardProxy.on("keydown.editor", function (event) { 3634 var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; //catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) 3635 if (Handsontable.helper.isPrintableChar(event.keyCode)) { 3636 if (!texteditor.isCellEdited && !ctrlDown) { //disregard CTRL-key shortcuts 3637 texteditor.beginEditing(instance, td, row, col, prop, keyboardProxy); 3638 event.stopImmediatePropagation(); 3639 } 3640 else if (ctrlDown) { 3641 if (texteditor.isCellEdited && event.keyCode === 65) { //CTRL + A 3642 event.stopPropagation(); 3643 } 3644 else if (texteditor.isCellEdited && event.keyCode === 88 && $.browser.opera) { //CTRL + X 3645 event.stopPropagation(); 3646 } 3647 else if (texteditor.isCellEdited && event.keyCode === 86 && $.browser.opera) { //CTRL + V 3648 event.stopPropagation(); 3649 } 3650 } 3651 return; 3652 } 3653 3654 switch (event.keyCode) { 3655 case 38: /* arrow up */ 3656 if (texteditor.isCellEdited) { 3657 texteditor.finishEditing(instance, td, row, col, prop, keyboardProxy, false); 3658 event.stopPropagation(); 3659 } 3660 break; 3661 3662 case 9: /* tab */ 3663 if (texteditor.isCellEdited) { 3664 texteditor.finishEditing(instance, td, row, col, prop, keyboardProxy, false); 3665 event.stopPropagation(); 3666 } 3667 event.preventDefault(); 3668 break; 3669 3670 case 39: /* arrow right */ 3671 if (texteditor.isCellEdited) { 3672 if (texteditor.getCaretPosition(keyboardProxy) === keyboardProxy.val().length) { 3673 texteditor.finishEditing(instance, td, row, col, prop, keyboardProxy, false); 3674 3675 } 3676 else { 3677 event.stopPropagation(); 3678 } 3679 } 3680 break; 3681 3682 case 37: /* arrow left */ 3683 if (texteditor.isCellEdited) { 3684 if (texteditor.getCaretPosition(keyboardProxy) === 0) { 3685 texteditor.finishEditing(instance, td, row, col, prop, keyboardProxy, false); 3686 } 3687 else { 3688 event.stopPropagation(); 3689 } 3690 } 3691 break; 3692 3693 case 8: /* backspace */ 3694 case 46: /* delete */ 3695 if (texteditor.isCellEdited) { 3696 event.stopPropagation(); 3697 } 3698 break; 3699 3700 case 40: /* arrow down */ 3701 if (texteditor.isCellEdited) { 3702 texteditor.finishEditing(instance, td, row, col, prop, keyboardProxy, false); 3703 event.stopPropagation(); 3704 } 3705 break; 3706 3707 case 27: /* ESC */ 3708 if (texteditor.isCellEdited) { 3709 texteditor.finishEditing(instance, td, row, col, prop, keyboardProxy, true); //hide edit field, restore old value, don't move selection, but refresh routines 3710 event.stopPropagation(); 3711 } 3712 break; 3713 3714 case 113: /* F2 */ 3715 if (!texteditor.isCellEdited) { 3716 texteditor.beginEditing(instance, td, row, col, prop, keyboardProxy, true); //show edit field 3717 event.stopPropagation(); 3718 event.preventDefault(); //prevent Opera from opening Go to Page dialog 3719 } 3720 break; 3721 3722 case 13: /* return/enter */ 3723 if (texteditor.isCellEdited) { 3724 var selected = instance.getSelected(); 3725 var isMultipleSelection = !(selected[0] === selected[2] && selected[1] === selected[3]); 3726 if ((event.ctrlKey && !isMultipleSelection) || event.altKey) { //if ctrl+enter or alt+enter, add new line 3727 keyboardProxy.val(keyboardProxy.val() + '\n'); 3728 keyboardProxy[0].focus(); 3729 event.stopPropagation(); 3730 } 3731 else { 3732 texteditor.finishEditing(instance, td, row, col, prop, keyboardProxy, false, ctrlDown); 3733 } 3734 } 3735 else if (instance.getSettings().enterBeginsEditing) { 3736 if ((ctrlDown && !selection.isMultiple()) || event.altKey) { //if ctrl+enter or alt+enter, add new line 3737 texteditor.beginEditing(instance, td, row, col, prop, keyboardProxy, true, '\n'); //show edit field 3738 } 3739 else { 3740 texteditor.beginEditing(instance, td, row, col, prop, keyboardProxy, true); //show edit field 3741 } 3742 event.stopPropagation(); 3743 } 3744 event.preventDefault(); //don't add newline to field 3745 break; 3746 3747 case 36: /* home */ 3748 event.stopPropagation(); 3749 break; 3750 3751 case 35: /* end */ 3752 event.stopPropagation(); 3753 break; 3754 } 3755 }); 3756 3757 function onDblClick() { 3758 keyboardProxy[0].focus(); 3759 texteditor.beginEditing(instance, td, row, col, prop, keyboardProxy, true); 3760 } 3761 3762 $current.on('dblclick.editor', onDblClick); 3763 instance.container.find('.htBorder.current').on('dblclick.editor', onDblClick); 3764 3765 return function (isCancelled) { 3766 texteditor.finishEditing(instance, td, row, col, prop, keyboardProxy, isCancelled); 3767 } 3768 }; 3769 function isAutoComplete(keyboardProxy) { 3770 var typeahead = keyboardProxy.data("typeahead"); 3771 if (typeahead && typeahead.$menu.is(":visible")) { 3772 return typeahead; 3773 } 3774 else { 3775 return false; 3776 } 3777 } 3778 3779 /** 3780 * Copied from bootstrap-typeahead.js for reference 3781 */ 3782 function defaultAutoCompleteHighlighter(item) { 3783 var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); 3784 return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { 3785 return '<strong>' + match + '</strong>'; 3786 }) 3787 } 3788 3789 /** 3790 * Autocomplete editor 3791 * @param {Object} instance Handsontable instance 3792 * @param {Element} td Table cell where to render 3793 * @param {Number} row 3794 * @param {Number} col 3795 * @param {String|Number} prop Row object property name 3796 * @param {Object} keyboardProxy jQuery element of keyboard proxy that contains current editing value 3797 * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) 3798 */ 3799 Handsontable.AutocompleteEditor = function (instance, td, row, col, prop, keyboardProxy, cellProperties) { 3800 var typeahead = keyboardProxy.data('typeahead') 3801 , dontHide = false; 3802 3803 if (!typeahead) { 3804 keyboardProxy.typeahead(); 3805 typeahead = keyboardProxy.data('typeahead'); 3806 } 3807 3808 typeahead.minLength = 0; 3809 typeahead.source = cellProperties.autoComplete.source(row, col); 3810 typeahead.highlighter = cellProperties.autoComplete.highlighter || defaultAutoCompleteHighlighter; 3811 3812 if (!typeahead._show) { 3813 typeahead._show = typeahead.show; 3814 typeahead._hide = typeahead.hide; 3815 typeahead._render = typeahead.render; 3816 } 3817 3818 typeahead.show = function () { 3819 if (keyboardProxy.parent().hasClass('htHidden')) { 3820 return; 3821 } 3822 return typeahead._show.call(this); 3823 }; 3824 3825 typeahead.hide = function () { 3826 if (!dontHide) { 3827 dontHide = false; //set to true by dblclick handler, otherwise appears and disappears immediately after double click 3828 return typeahead._hide.call(this); 3829 } 3830 }; 3831 3832 typeahead.lookup = function () { 3833 var items; 3834 this.query = this.$element.val(); 3835 items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source; 3836 return items ? this.process(items) : this; 3837 }; 3838 3839 typeahead.matcher = function () { 3840 return true; 3841 }; 3842 3843 typeahead.select = function () { 3844 var val = this.$menu.find('.active').attr('data-value') || keyboardProxy.val(); 3845 destroyer(true); 3846 instance.setDataAtCell(row, prop, typeahead.updater(val)); 3847 return this.hide(); 3848 }; 3849 3850 typeahead.render = function (items) { 3851 typeahead._render.call(this, items); 3852 if (cellProperties.autoComplete.strict) { 3853 this.$menu.find('li:eq(0)').removeClass('active'); 3854 } 3855 return this; 3856 }; 3857 3858 keyboardProxy.on("keydown.editor", function (event) { 3859 switch (event.keyCode) { 3860 case 27: /* ESC */ 3861 dontHide = false; 3862 break; 3863 3864 case 38: /* arrow up */ 3865 case 40: /* arrow down */ 3866 case 9: /* tab */ 3867 case 13: /* return/enter */ 3868 if (isAutoComplete(keyboardProxy)) { 3869 event.stopImmediatePropagation(); 3870 } 3871 event.preventDefault(); 3872 } 3873 }); 3874 3875 keyboardProxy.on("keyup.editor", function (event) { 3876 switch (event.keyCode) { 3877 case 9: /* tab */ 3878 case 13: /* return/enter */ 3879 if (!isAutoComplete(keyboardProxy)) { 3880 var ev = $.Event('keyup'); 3881 ev.keyCode = 113; //113 triggers lookup, in contrary to 13 or 9 which only trigger hide 3882 keyboardProxy.trigger(ev); 3883 } 3884 else { 3885 setTimeout(function () { //so pressing enter will move one row down after change is applied by 'select' above 3886 var ev = $.Event('keydown'); 3887 ev.keyCode = event.keyCode; 3888 keyboardProxy.parent().trigger(ev); 3889 }, 10); 3890 } 3891 break; 3892 3893 default: 3894 if (!Handsontable.helper.isPrintableChar(event.keyCode)) { //otherwise Del or F12 would open suggestions list 3895 event.stopImmediatePropagation(); 3896 } 3897 } 3898 } 3899 ); 3900 3901 var textDestroyer = Handsontable.TextEditor(instance, td, row, col, prop, keyboardProxy, cellProperties); 3902 3903 function onDblClick() { 3904 dontHide = true; 3905 setTimeout(function () { //otherwise is misaligned in IE9 3906 keyboardProxy.data('typeahead').lookup(); 3907 }, 1); 3908 } 3909 3910 $(td).on('dblclick.editor', onDblClick); 3911 instance.container.find('.htBorder.current').on('dblclick.editor', onDblClick); 3912 3913 var destroyer = function (isCancelled) { 3914 textDestroyer(isCancelled); 3915 typeahead.source = []; 3916 dontHide = false; 3917 if (isAutoComplete(keyboardProxy)) { 3918 isAutoComplete(keyboardProxy).hide(); 3919 } 3920 }; 3921 3922 return destroyer; 3923 }; 3924 function toggleCheckboxCell(instance, row, prop, cellProperties) { 3925 if (Handsontable.helper.stringify(instance.getDataAtCell(row, prop)) === Handsontable.helper.stringify(cellProperties.checkedTemplate)) { 3926 instance.setDataAtCell(row, prop, cellProperties.uncheckedTemplate); 3927 } 3928 else { 3929 instance.setDataAtCell(row, prop, cellProperties.checkedTemplate); 3930 } 3931 } 3932 3933 /** 3934 * Checkbox editor 3935 * @param {Object} instance Handsontable instance 3936 * @param {Element} td Table cell where to render 3937 * @param {Number} row 3938 * @param {Number} col 3939 * @param {String|Number} prop Row object property name 3940 * @param {Object} keyboardProxy jQuery element of keyboard proxy that contains current editing value 3941 * @param {Object} cellProperties Cell properites (shared by cell renderer and editor) 3942 */ 3943 Handsontable.CheckboxEditor = function (instance, td, row, col, prop, keyboardProxy, cellProperties) { 3944 if (typeof cellProperties === "undefined") { 3945 cellProperties = {}; 3946 } 3947 if (typeof cellProperties.checkedTemplate === "undefined") { 3948 cellProperties.checkedTemplate = true; 3949 } 3950 if (typeof cellProperties.uncheckedTemplate === "undefined") { 3951 cellProperties.uncheckedTemplate = false; 3952 } 3953 3954 keyboardProxy.on("keydown.editor", function (event) { 3955 var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; //catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) 3956 if (!ctrlDown && Handsontable.helper.isPrintableChar(event.keyCode)) { 3957 toggleCheckboxCell(instance, row, prop, cellProperties); 3958 event.stopPropagation(); 3959 } 3960 }); 3961 3962 function onDblClick() { 3963 toggleCheckboxCell(instance, row, prop, cellProperties); 3964 } 3965 3966 var $td = $(td); 3967 $td.on('dblclick.editor', onDblClick); 3968 instance.container.find('.htBorder.current').on('dblclick.editor', onDblClick); 3969 3970 return function () { 3971 keyboardProxy.off(".editor"); 3972 $td.off(".editor"); 3973 instance.container.find('.htBorder.current').off(".editor"); 3974 } 3975 }; 3976 Handsontable.AutocompleteCell = { 3977 renderer: Handsontable.AutocompleteRenderer, 3978 editor: Handsontable.AutocompleteEditor 3979 }; 3980 3981 Handsontable.CheckboxCell = { 3982 renderer: Handsontable.CheckboxRenderer, 3983 editor: Handsontable.CheckboxEditor 3984 }; 3985 3986 Handsontable.TextCell = { 3987 renderer: Handsontable.TextRenderer, 3988 editor: Handsontable.TextEditor 3989 }; 3990 Handsontable.PluginHooks = { 3991 hooks: { 3992 afterInit: [] 3993 }, 3994 3995 push: function(hook, fn){ 3996 this.hooks[hook].push(fn); 3997 }, 3998 3999 unshift: function(hook, fn){ 4000 this.hooks[hook].unshift(fn); 4001 }, 4002 4003 run: function(instance, hook){ 4004 for(var i = 0, ilen = this.hooks[hook].length; i<ilen; i++) { 4005 this.hooks[hook][i].apply(instance); 4006 } 4007 } 4008 }; 4009 function createContextMenu() { 4010 var instance = this 4011 , defaultOptions = { 4012 selector: "#" + instance.rootElement.attr('id') + ' table, #' + instance.rootElement.attr('id') + ' div', 4013 trigger: 'right', 4014 callback: onContextClick 4015 }, 4016 allItems = { 4017 "row_above": {name: "Insert row above", disabled: isDisabled}, 4018 "row_below": {name: "Insert row below", disabled: isDisabled}, 4019 "hsep1": "---------", 4020 "col_left": {name: "Insert column on the left", disabled: isDisabled}, 4021 "col_right": {name: "Insert column on the right", disabled: isDisabled}, 4022 "hsep2": "---------", 4023 "remove_row": {name: "Remove row", disabled: isDisabled}, 4024 "remove_col": {name: "Remove column", disabled: isDisabled}, 4025 "hsep3": "---------", 4026 "undo": {name: "Undo", disabled: function () { 4027 return !instance.isUndoAvailable(); 4028 }}, 4029 "redo": {name: "Redo", disabled: function () { 4030 return !instance.isRedoAvailable(); 4031 }} 4032 } 4033 , options = {} 4034 , i 4035 , ilen 4036 , settings = instance.getSettings(); 4037 4038 function onContextClick(key) { 4039 var corners = instance.getSelected(); //[top left row, top left col, bottom right row, bottom right col] 4040 4041 switch (key) { 4042 case "row_above": 4043 instance.alter("insert_row", corners[0]); 4044 break; 4045 4046 case "row_below": 4047 instance.alter("insert_row", corners[2] + 1); 4048 break; 4049 4050 case "col_left": 4051 instance.alter("insert_col", corners[1]); 4052 break; 4053 4054 case "col_right": 4055 instance.alter("insert_col", corners[3] + 1); 4056 break; 4057 4058 case "remove_row": 4059 instance.alter(key, corners[0], corners[2]); 4060 break; 4061 4062 case "remove_col": 4063 instance.alter(key, corners[1], corners[3]); 4064 break; 4065 4066 case "undo": 4067 instance.undo(); 4068 break; 4069 4070 case "redo": 4071 instance.redo(); 4072 break; 4073 } 4074 } 4075 4076 function isDisabled(key) { 4077 if (instance.blockedCols.main.find('th.htRowHeader.active').length && (key === "remove_col" || key === "col_left" || key === "col_right")) { 4078 return true; 4079 } 4080 else if (instance.blockedRows.main.find('th.htColHeader.active').length && (key === "remove_row" || key === "row_above" || key === "row_below")) { 4081 return true; 4082 } 4083 else { 4084 return false; 4085 } 4086 } 4087 4088 if (!settings.contextMenu) { 4089 return; 4090 } 4091 else if (settings.contextMenu === true) { //contextMenu is true 4092 options.items = allItems; 4093 } 4094 else if (Object.prototype.toString.apply(settings.contextMenu) === '[object Array]') { //contextMenu is an array 4095 options.items = {}; 4096 for (i = 0, ilen = settings.contextMenu.length; i < ilen; i++) { 4097 var key = settings.contextMenu[i]; 4098 if (typeof allItems[key] === 'undefined') { 4099 throw new Error('Context menu key "' + key + '" is not recognised'); 4100 } 4101 options.items[key] = allItems[key]; 4102 } 4103 } 4104 else if (Object.prototype.toString.apply(settings.contextMenu) === '[object Object]') { //contextMenu is an options object as defined in http://medialize.github.com/jQuery-contextMenu/docs.html 4105 options = settings.contextMenu; 4106 if (options.items) { 4107 for (i in options.items) { 4108 if (options.items.hasOwnProperty(i) && allItems[i]) { 4109 if (typeof options.items[i] === 'string') { 4110 options.items[i] = allItems[i]; 4111 } 4112 else { 4113 options.items[i] = $.extend(true, allItems[i], options.items[i]); 4114 } 4115 } 4116 } 4117 } 4118 else { 4119 options.items = allItems; 4120 } 4121 4122 if (options.callback) { 4123 var handsontableCallback = defaultOptions.callback; 4124 var customCallback = options.callback; 4125 options.callback = function (key, options) { 4126 handsontableCallback(key, options); 4127 customCallback(key, options); 4128 } 4129 } 4130 } 4131 4132 if (!instance.rootElement.attr('id')) { 4133 throw new Error("Handsontable container must have an id"); 4134 } 4135 4136 $.contextMenu($.extend(true, defaultOptions, options)); 4137 } 4138 4139 Handsontable.PluginHooks.push('afterInit', createContextMenu); 4140 /* 4141 * jQuery.fn.autoResize 1.1+ 4142 * -- 4143 * https://github.com/warpech/jQuery.fn.autoResize 4144 * 4145 * This fork differs from others in a way that it autoresizes textarea in 2-dimensions (horizontally and vertically). 4146 * It was originally forked from alexbardas's repo but maybe should be merged with dpashkevich's repo in future. 4147 * 4148 * originally forked from: 4149 * https://github.com/jamespadolsey/jQuery.fn.autoResize 4150 * which is now located here: 4151 * https://github.com/alexbardas/jQuery.fn.autoResize 4152 * though the mostly maintained for is here: 4153 * https://github.com/dpashkevich/jQuery.fn.autoResize/network 4154 * 4155 * -- 4156 * This program is free software. It comes without any warranty, to 4157 * the extent permitted by applicable law. You can redistribute it 4158 * and/or modify it under the terms of the Do What The Fuck You Want 4159 * To Public License, Version 2, as published by Sam Hocevar. See 4160 * http://sam.zoy.org/wtfpl/COPYING for more details. */ 4161 4162 (function($){ 4163 4164 autoResize.defaults = { 4165 onResize: function(){}, 4166 animate: { 4167 duration: 200, 4168 complete: function(){} 4169 }, 4170 extraSpace: 50, 4171 minHeight: 'original', 4172 maxHeight: 500, 4173 minWidth: 'original', 4174 maxWidth: 500 4175 }; 4176 4177 autoResize.cloneCSSProperties = [ 4178 'lineHeight', 'textDecoration', 'letterSpacing', 4179 'fontSize', 'fontFamily', 'fontStyle', 'fontWeight', 4180 'textTransform', 'textAlign', 'direction', 'wordSpacing', 'fontSizeAdjust', 4181 'padding' 4182 ]; 4183 4184 autoResize.cloneCSSValues = { 4185 position: 'absolute', 4186 top: -9999, 4187 left: -9999, 4188 opacity: 0, 4189 overflow: 'hidden', 4190 border: '1px solid black', 4191 padding: '0.49em' //this must be about the width of caps W character 4192 }; 4193 4194 autoResize.resizableFilterSelector = 'textarea,input:not(input[type]),input[type=text],input[type=password]'; 4195 4196 autoResize.AutoResizer = AutoResizer; 4197 4198 $.fn.autoResize = autoResize; 4199 4200 function autoResize(config) { 4201 this.filter(autoResize.resizableFilterSelector).each(function(){ 4202 new AutoResizer( $(this), config ); 4203 }); 4204 return this; 4205 } 4206 4207 function AutoResizer(el, config) { 4208 4209 if(this.clones) return; 4210 4211 this.config = $.extend({}, autoResize.defaults, config); 4212 4213 this.el = el; 4214 4215 this.nodeName = el[0].nodeName.toLowerCase(); 4216 4217 this.previousScrollTop = null; 4218 4219 if (config.maxWidth === 'original') config.maxWidth = el.width(); 4220 if (config.minWidth === 'original') config.minWidth = el.width(); 4221 if (config.maxHeight === 'original') config.maxHeight = el.height(); 4222 if (config.minHeight === 'original') config.minHeight = el.height(); 4223 4224 if (this.nodeName === 'textarea') { 4225 el.css({ 4226 resize: 'none', 4227 overflowY: 'hidden' 4228 }); 4229 } 4230 4231 el.data('AutoResizer', this); 4232 4233 this.createClone(); 4234 this.injectClone(); 4235 this.bind(); 4236 4237 } 4238 4239 AutoResizer.prototype = { 4240 4241 bind: function() { 4242 4243 var check = $.proxy(function(){ 4244 this.check(); 4245 return true; 4246 }, this); 4247 4248 this.unbind(); 4249 4250 this.el 4251 .bind('keyup.autoResize', check) 4252 //.bind('keydown.autoResize', check) 4253 .bind('change.autoResize', check); 4254 4255 this.check(null, true); 4256 4257 }, 4258 4259 unbind: function() { 4260 this.el.unbind('.autoResize'); 4261 }, 4262 4263 createClone: function() { 4264 4265 var el = this.el, 4266 self = this, 4267 config = this.config; 4268 4269 this.clones = $(); 4270 4271 if (config.minHeight !== 'original' || config.maxHeight !== 'original') { 4272 this.hClone = el.clone().height('auto'); 4273 this.clones = this.clones.add(this.hClone); 4274 } 4275 if (config.minWidth !== 'original' || config.maxWidth !== 'original') { 4276 this.wClone = $('<div/>').width('auto').css({ 4277 whiteSpace: 'nowrap', 4278 'float': 'left' 4279 }); 4280 this.clones = this.clones.add(this.wClone); 4281 } 4282 4283 $.each(autoResize.cloneCSSProperties, function(i, p){ 4284 self.clones.css(p, el.css(p)); 4285 }); 4286 4287 this.clones 4288 .removeAttr('name') 4289 .removeAttr('id') 4290 .attr('tabIndex', -1) 4291 .css(autoResize.cloneCSSValues); 4292 4293 }, 4294 4295 check: function(e, immediate) { 4296 4297 var config = this.config, 4298 wClone = this.wClone, 4299 hClone = this.hClone, 4300 el = this.el, 4301 value = el.val(); 4302 4303 if (wClone) { 4304 4305 wClone.text(value); 4306 4307 // Calculate new width + whether to change 4308 var cloneWidth = wClone.outerWidth(), 4309 newWidth = (cloneWidth + config.extraSpace) >= config.minWidth ? 4310 cloneWidth + config.extraSpace : config.minWidth, 4311 currentWidth = el.width(); 4312 4313 newWidth = Math.min(newWidth, config.maxWidth); 4314 4315 if ( 4316 (newWidth < currentWidth && newWidth >= config.minWidth) || 4317 (newWidth >= config.minWidth && newWidth <= config.maxWidth) 4318 ) { 4319 4320 config.onResize.call(el); 4321 4322 el.scrollLeft(0); 4323 4324 config.animate && !immediate ? 4325 el.stop(1,1).animate({ 4326 width: newWidth 4327 }, config.animate) 4328 : el.width(newWidth); 4329 4330 } 4331 4332 } 4333 4334 if (hClone) { 4335 4336 if (newWidth) { 4337 hClone.width(newWidth); 4338 } 4339 4340 hClone.height(0).val(value).scrollTop(10000); 4341 4342 var scrollTop = hClone[0].scrollTop + config.extraSpace; 4343 4344 // Don't do anything if scrollTop hasen't changed: 4345 if (this.previousScrollTop === scrollTop) { 4346 return; 4347 } 4348 4349 this.previousScrollTop = scrollTop; 4350 4351 if (scrollTop >= config.maxHeight) { 4352 el.css('overflowY', ''); 4353 return; 4354 } 4355 4356 el.css('overflowY', 'hidden'); 4357 4358 if (scrollTop < config.minHeight) { 4359 scrollTop = config.minHeight; 4360 } 4361 4362 config.onResize.call(el); 4363 4364 // Either animate or directly apply height: 4365 config.animate && !immediate ? 4366 el.stop(1,1).animate({ 4367 height: scrollTop 4368 }, config.animate) 4369 : el.height(scrollTop); 4370 } 4371 }, 4372 4373 destroy: function() { 4374 this.unbind(); 4375 this.el.removeData('AutoResizer'); 4376 this.clones.remove(); 4377 delete this.el; 4378 delete this.hClone; 4379 delete this.wClone; 4380 delete this.clones; 4381 }, 4382 4383 injectClone: function() { 4384 ( 4385 autoResize.cloneContainer || 4386 (autoResize.cloneContainer = $('<arclones/>').appendTo('body')) 4387 ).append(this.clones); 4388 } 4389 4390 }; 4391 4392 })(jQuery); 4393 /*! Copyright (c) 2011 Brandon Aaron (http://brandonaaron.net) 4394 * Licensed under the MIT License (LICENSE.txt). 4395 * 4396 * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers. 4397 * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix. 4398 * Thanks to: Seamus Leahy for adding deltaX and deltaY 4399 * 4400 * Version: 3.0.6 4401 * 4402 * Requires: 1.2.2+ 4403 */ 4404 4405 (function($) { 4406 4407 var types = ['DOMMouseScroll', 'mousewheel']; 4408 4409 if ($.event.fixHooks) { 4410 for ( var i=types.length; i; ) { 4411 $.event.fixHooks[ types[--i] ] = $.event.mouseHooks; 4412 } 4413 } 4414 4415 $.event.special.mousewheel = { 4416 setup: function() { 4417 if ( this.addEventListener ) { 4418 for ( var i=types.length; i; ) { 4419 this.addEventListener( types[--i], handler, false ); 4420 } 4421 } else { 4422 this.onmousewheel = handler; 4423 } 4424 }, 4425 4426 teardown: function() { 4427 if ( this.removeEventListener ) { 4428 for ( var i=types.length; i; ) { 4429 this.removeEventListener( types[--i], handler, false ); 4430 } 4431 } else { 4432 this.onmousewheel = null; 4433 } 4434 } 4435 }; 4436 4437 $.fn.extend({ 4438 mousewheel: function(fn) { 4439 return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel"); 4440 }, 4441 4442 unmousewheel: function(fn) { 4443 return this.unbind("mousewheel", fn); 4444 } 4445 }); 4446 4447 4448 function handler(event) { 4449 var orgEvent = event || window.event, args = [].slice.call( arguments, 1 ), delta = 0, returnValue = true, deltaX = 0, deltaY = 0; 4450 event = $.event.fix(orgEvent); 4451 event.type = "mousewheel"; 4452 4453 // Old school scrollwheel delta 4454 if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta/120; } 4455 if ( orgEvent.detail ) { delta = -orgEvent.detail/3; } 4456 4457 // New school multidimensional scroll (touchpads) deltas 4458 deltaY = delta; 4459 4460 // Gecko 4461 if ( orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) { 4462 deltaY = 0; 4463 deltaX = -1*delta; 4464 } 4465 4466 // Webkit 4467 if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY/120; } 4468 if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = -1*orgEvent.wheelDeltaX/120; } 4469 4470 // Add event and delta to the front of the arguments 4471 args.unshift(event, delta, deltaX, deltaY); 4472 4473 return ($.event.dispatch || $.event.handle).apply(this, args); 4474 } 4475 4476 })(jQuery); 4477 4478 /** 4479 * SheetClip - Spreadsheet Clipboard Parser 4480 * version 0.1 4481 * 4482 * This tiny library transforms JavaScript arrays to strings that are pasteable by LibreOffice, OpenOffice, 4483 * Google Docs and Microsoft Excel. 4484 * 4485 * Copyright 2012, Marcin Warpechowski 4486 * Licensed under the MIT license. 4487 * http://github.com/warpech/sheetclip/ 4488 */ 4489 /*jslint white: true*/ 4490 (function (global) { 4491 "use strict"; 4492 4493 var UNDEFINED = (function () { 4494 }()); 4495 4496 function countQuotes(str) { 4497 return str.split('"').length - 1; 4498 } 4499 4500 global.SheetClip = { 4501 parse: function (str) { 4502 var r, rlen, rows, arr = [], a = 0, c, clen, multiline, last; 4503 rows = str.split('\n'); 4504 if (rows.length > 1 && rows[rows.length - 1] === '') { 4505 rows.pop(); 4506 } 4507 for (r = 0, rlen = rows.length; r < rlen; r += 1) { 4508 rows[r] = rows[r].split('\t'); 4509 for (c = 0, clen = rows[r].length; c < clen; c += 1) { 4510 if (!arr[a]) { 4511 arr[a] = []; 4512 } 4513 if (multiline && c === 0) { 4514 last = arr[a].length - 1; 4515 arr[a][last] = arr[a][last] + '\n' + rows[r][0]; 4516 if (multiline && countQuotes(rows[r][0]) % 2 === 1) { 4517 multiline = false; 4518 arr[a][last] = arr[a][last].substring(0, arr[a][last].length - 1).replace(/""/g, '"'); 4519 } 4520 } 4521 else { 4522 if (c === clen - 1 && rows[r][c].indexOf('"') === 0) { 4523 arr[a].push(rows[r][c].substring(1).replace(/""/g, '"')); 4524 multiline = true; 4525 } 4526 else { 4527 arr[a].push(rows[r][c].replace(/""/g, '"')); 4528 multiline = false; 4529 } 4530 } 4531 } 4532 if(!multiline) { 4533 a += 1; 4534 } 4535 } 4536 return arr; 4537 }, 4538 4539 stringify: function (arr) { 4540 var r, rlen, c, clen, str = '', val; 4541 for (r = 0, rlen = arr.length; r < rlen; r += 1) { 4542 for (c = 0, clen = arr[r].length; c < clen; c += 1) { 4543 if (c > 0) { 4544 str += '\t'; 4545 } 4546 val = arr[r][c]; 4547 if (typeof val === 'string') { 4548 if (val.indexOf('\n') > -1) { 4549 str += '"' + val.replace(/"/g, '""') + '"'; 4550 } 4551 else { 4552 str += val; 4553 } 4554 } 4555 else if (val === null || val === UNDEFINED) { 4556 str += ''; 4557 } 4558 else { 4559 str += val; 4560 } 4561 } 4562 str += '\n'; 4563 } 4564 return str; 4565 } 4566 }; 4567 }(window)); 4568 })(jQuery, window, Handsontable);
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:08:37 2014 | Cross-referenced by PHPXref 0.7.1 |