[ Index ]

PHP Cross Reference of vtigercrm-6.1.0

title

Body

[close]

/libraries/jquery/handsontable/ -> jquery.handsontable.js (source)

   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('&nbsp;');
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('&nbsp;');
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 '&nbsp;<span class="small">' + str + '</span>&nbsp;';
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('&nbsp;');
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;"); //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">&#x25BC;</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('&nbsp;');
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);


Generated: Fri Nov 28 20:08:37 2014 Cross-referenced by PHPXref 0.7.1