Parcourir la source

feat(handsontable): init

qinlaiqiao il y a 5 ans
Parent
commit
97733c717f
100 fichiers modifiés avec 19568 ajouts et 0 suppressions
  1. 3 0
      handsontable/.bowerrc
  2. 77 0
      handsontable/.config/base.js
  3. 85 0
      handsontable/.config/bin/trigger-on-stdout-change.js
  4. 90 0
      handsontable/.config/development.js
  5. 109 0
      handsontable/.config/languages-development.js
  6. 40 0
      handsontable/.config/languages-production.js
  7. 11 0
      handsontable/.config/loader/empty-loader.js
  8. 25 0
      handsontable/.config/loader/exports-to-window-loader.js
  9. 37 0
      handsontable/.config/plugin/jasmine-html.js
  10. 22 0
      handsontable/.config/plugin/template.ejs
  11. 86 0
      handsontable/.config/production.js
  12. 67 0
      handsontable/.config/test-e2e.js
  13. 48 0
      handsontable/.config/test-mobile.js
  14. 47 0
      handsontable/.config/test-production.js
  15. 62 0
      handsontable/.config/test-walkontable.js
  16. 44 0
      handsontable/.config/walkontable.js
  17. 55 0
      handsontable/.config/watch.js
  18. 17 0
      handsontable/.editorconfig
  19. 12 0
      handsontable/.eslintignore
  20. 95 0
      handsontable/.eslintrc.js
  21. 2 0
      handsontable/.gitattributes
  22. 21 0
      handsontable/.gitignore
  23. 20 0
      handsontable/.npmignore
  24. 20 0
      handsontable/.travis.yml
  25. 1 0
      handsontable/CHANGELOG.md
  26. 1 0
      handsontable/CNAME
  27. 46 0
      handsontable/CODE_OF_CONDUCT.md
  28. 17 0
      handsontable/CONTRIBUTING.md
  29. 23 0
      handsontable/LICENSE
  30. 172 0
      handsontable/README.md
  31. 174 0
      handsontable/babel.config.js
  32. 44 0
      handsontable/bower.json
  33. 2778 0
      handsontable/handsontable.d.ts
  34. 37 0
      handsontable/handsontable.jquery.json
  35. 12 0
      handsontable/hot.config.js
  36. 119 0
      handsontable/lib/SheetClip/SheetClip.js
  37. 181 0
      handsontable/lib/autoResize/autoResize.js
  38. 631 0
      handsontable/lib/jsonpatch/json-patch-duplex.js
  39. 138 0
      handsontable/package.json
  40. 148 0
      handsontable/src/3rdparty/walkontable/css/bootstrap.css
  41. 164 0
      handsontable/src/3rdparty/walkontable/css/walkontable.css
  42. 15 0
      handsontable/src/3rdparty/walkontable/package.json
  43. 674 0
      handsontable/src/3rdparty/walkontable/src/border.js
  44. 280 0
      handsontable/src/3rdparty/walkontable/src/calculator/viewportColumns.js
  45. 150 0
      handsontable/src/3rdparty/walkontable/src/calculator/viewportRows.js
  46. 113 0
      handsontable/src/3rdparty/walkontable/src/cell/coords.js
  47. 585 0
      handsontable/src/3rdparty/walkontable/src/cell/range.js
  48. 267 0
      handsontable/src/3rdparty/walkontable/src/core.js
  49. 342 0
      handsontable/src/3rdparty/walkontable/src/event.js
  50. 81 0
      handsontable/src/3rdparty/walkontable/src/filter/column.js
  51. 81 0
      handsontable/src/3rdparty/walkontable/src/filter/row.js
  52. 58 0
      handsontable/src/3rdparty/walkontable/src/index.js
  53. 276 0
      handsontable/src/3rdparty/walkontable/src/overlay/_base.js
  54. 313 0
      handsontable/src/3rdparty/walkontable/src/overlay/bottom.js
  55. 100 0
      handsontable/src/3rdparty/walkontable/src/overlay/bottomLeftCorner.js
  56. 27 0
      handsontable/src/3rdparty/walkontable/src/overlay/debug.js
  57. 312 0
      handsontable/src/3rdparty/walkontable/src/overlay/left.js
  58. 362 0
      handsontable/src/3rdparty/walkontable/src/overlay/top.js
  59. 78 0
      handsontable/src/3rdparty/walkontable/src/overlay/topLeftCorner.js
  60. 586 0
      handsontable/src/3rdparty/walkontable/src/overlays.js
  61. 301 0
      handsontable/src/3rdparty/walkontable/src/scroll.js
  62. 282 0
      handsontable/src/3rdparty/walkontable/src/selection.js
  63. 175 0
      handsontable/src/3rdparty/walkontable/src/settings.js
  64. 695 0
      handsontable/src/3rdparty/walkontable/src/table.js
  65. 662 0
      handsontable/src/3rdparty/walkontable/src/tableRenderer.js
  66. 489 0
      handsontable/src/3rdparty/walkontable/src/viewport.js
  67. 34 0
      handsontable/src/3rdparty/walkontable/test/SpecRunner.html
  68. 273 0
      handsontable/src/3rdparty/walkontable/test/helpers/common.js
  69. 8 0
      handsontable/src/3rdparty/walkontable/test/helpers/index.js
  70. 47 0
      handsontable/src/3rdparty/walkontable/test/helpers/jasmine-bridge-reporter.js
  71. 2 0
      handsontable/src/3rdparty/walkontable/test/lib/jquery.min.js
  72. 337 0
      handsontable/src/3rdparty/walkontable/test/lib/jquery.simulate.js
  73. 411 0
      handsontable/src/3rdparty/walkontable/test/spec/border.spec.js
  74. 226 0
      handsontable/src/3rdparty/walkontable/test/spec/calculator/viewportColumns.spec.js
  75. 149 0
      handsontable/src/3rdparty/walkontable/test/spec/calculator/viewportRows.spec.js
  76. 73 0
      handsontable/src/3rdparty/walkontable/test/spec/cell/coords.spec.js
  77. 1374 0
      handsontable/src/3rdparty/walkontable/test/spec/cell/range.spec.js
  78. 197 0
      handsontable/src/3rdparty/walkontable/test/spec/core.spec.js
  79. 547 0
      handsontable/src/3rdparty/walkontable/test/spec/event.spec.js
  80. 29 0
      handsontable/src/3rdparty/walkontable/test/spec/filter/column.spec.js
  81. 73 0
      handsontable/src/3rdparty/walkontable/test/spec/filter/row.spec.js
  82. 7 0
      handsontable/src/3rdparty/walkontable/test/spec/index.js
  83. 947 0
      handsontable/src/3rdparty/walkontable/test/spec/scroll.spec.js
  84. 56 0
      handsontable/src/3rdparty/walkontable/test/spec/scrollbar.spec.js
  85. 96 0
      handsontable/src/3rdparty/walkontable/test/spec/scrollbarNative.spec.js
  86. 349 0
      handsontable/src/3rdparty/walkontable/test/spec/selection.spec.js
  87. 127 0
      handsontable/src/3rdparty/walkontable/test/spec/settings/columnHeaders.spec.js
  88. 59 0
      handsontable/src/3rdparty/walkontable/test/spec/settings/preventOverflow.spec.js
  89. 111 0
      handsontable/src/3rdparty/walkontable/test/spec/settings/rowHeaders.spec.js
  90. 205 0
      handsontable/src/3rdparty/walkontable/test/spec/settings/stretchH.spec.js
  91. 593 0
      handsontable/src/3rdparty/walkontable/test/spec/table.spec.js
  92. 11 0
      handsontable/src/cellTypes/autocompleteType.js
  93. 9 0
      handsontable/src/cellTypes/checkboxType.js
  94. 12 0
      handsontable/src/cellTypes/dateType.js
  95. 12 0
      handsontable/src/cellTypes/dropdownType.js
  96. 10 0
      handsontable/src/cellTypes/handsontableType.js
  97. 77 0
      handsontable/src/cellTypes/index.js
  98. 12 0
      handsontable/src/cellTypes/numericType.js
  99. 10 0
      handsontable/src/cellTypes/passwordType.js
  100. 0 0
      handsontable/src/cellTypes/textType.js

+ 3 - 0
handsontable/.bowerrc

@@ -0,0 +1,3 @@
+{
+  "directory": "demo/bower_components"
+}

+ 77 - 0
handsontable/.config/base.js

@@ -0,0 +1,77 @@
+'use strict';
+
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
+const ProgressBarPlugin = require('progress-bar-webpack-plugin');
+const path = require('path');
+const fs = require('fs');
+const webpack = require('webpack');
+
+let licenseBody = fs.readFileSync(path.resolve(__dirname, '../LICENSE'), 'utf8');
+
+licenseBody += '\nVersion: ' + process.env.HOT_VERSION;
+licenseBody += '\nRelease date: ' + process.env.HOT_RELEASE_DATE + ' (built at ' + process.env.HOT_BUILD_DATE + ')';
+
+module.exports.create = function create(envArgs) {
+  const config = {
+    devtool: false,
+    output: {
+      library: 'Handsontable',
+      libraryTarget: 'umd',
+      libraryExport: 'default',
+      umdNamedDefine: true,
+      path: path.resolve(__dirname, '../dist'),
+    },
+    resolve: {
+      alias: {},
+    },
+    module: {
+      rules: [
+        {
+          test: /\.css$/,
+          loader: ExtractTextPlugin.extract({
+            fallback: 'style-loader',
+            use: 'css-loader',
+          }),
+        },
+        {
+          test: [
+            // Disable loading languages from numbro and moment into final bundle
+            /numbro\/languages/,
+            /moment\/locale/,
+          ],
+          loader: path.resolve(__dirname, 'loader/empty-loader.js'),
+        },
+        {
+          test: /\.js$/,
+          loader: 'babel-loader',
+          exclude: [
+            /node_modules/,
+          ],
+          options: {
+            cacheDirectory: false, // Disable cache. Necessary for injected variables into source code via hot.config.js
+          },
+        },
+      ]
+    },
+    plugins: [
+      new ProgressBarPlugin({
+        format: '  build [:bar] \u001b[32m:percent\u001b[0m (:elapsed seconds)',
+        summary: false,
+      }),
+      // This helps ensure the builds are consistent if source code hasn't changed
+      new webpack.optimize.OccurrenceOrderPlugin(),
+      new webpack.BannerPlugin(licenseBody),
+      new webpack.DefinePlugin({
+        '__ENV_ARGS__': JSON.stringify(envArgs),
+      }),
+    ],
+    node: {
+      global: false,
+      process: false,
+      Buffer: false,
+      setImmediate: false,
+    },
+  };
+
+  return [config];
+}

+ 85 - 0
handsontable/.config/bin/trigger-on-stdout-change.js

@@ -0,0 +1,85 @@
+var treeKill = require('tree-kill');
+var spawnCommand = require('spawn-command');
+var supportsColor = require('supports-color');
+
+var CMD = process.argv[2];
+var CMD_TO_TRIGGER = process.argv[3];
+
+var IS_WINDOWS = /^win/.test(process.platform);
+var waitForFirstBuild = true;
+var triggeredChild;
+var delayedTrigger;
+
+setTimeout(function() {
+  waitForFirstBuild = false;
+}, 5000);
+
+run(CMD, CMD_TO_TRIGGER);
+
+function run(commands, commandToTrigger, spawnOpts) {
+  spawnOpts = spawnOpts || {};
+
+  if (IS_WINDOWS) {
+    spawnOpts.detached = false;
+  }
+  spawnOpts.env = Object.assign({FORCE_COLOR: supportsColor.level}, process.env);
+
+  var child;
+  try {
+    child = spawnCommand(commands, spawnOpts);
+  } catch(ex) {
+    console.log('Error occured when executing command: ' + commands);
+    console.log(ex.stack);
+    process.exit(1);
+  }
+
+  if (commandToTrigger) {
+    child.stdout.on('data', function(data) {
+      if (waitForFirstBuild) {
+        return;
+      }
+      if (triggeredChild) {
+        treeKill(triggeredChild.pid);
+      }
+      if (!delayedTrigger) {
+        delayedTrigger = debounce(function() {
+          triggeredChild = run(commandToTrigger);
+        }, 1000);
+      }
+      delayedTrigger();
+    });
+  }
+
+  child.stdout.pipe(process.stdout);
+  child.stderr.pipe(process.stderr);
+
+  if (commandToTrigger) {
+    ['SIGINT', 'SIGTERM'].forEach(function(signal) {
+      process.on(signal, function() {
+        treeKill(child.pid, signal);
+      });
+    });
+  }
+
+  return child;
+}
+
+function debounce(func, wait) {
+  var lastTimer = null;
+  var result;
+
+  function _debounce() {
+    var args = arguments;
+
+    if (lastTimer) {
+      clearTimeout(lastTimer);
+    }
+    lastTimer = setTimeout(function() {
+      result = func.apply(this, args);
+    }, wait);
+
+    return result;
+  }
+
+  return _debounce;
+}

+ 90 - 0
handsontable/.config/development.js

@@ -0,0 +1,90 @@
+'use strict';
+
+/**
+ * Config responsible for building Handsontable `dist/` files:
+ *  - handsontable.js
+ *  - handsontable.css
+ *  - handsontable.full.js
+ *  - handsontable.full.css
+ */
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
+const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+const path = require('path');
+const webpack = require('webpack');
+const configFactory = require('./base');
+
+const PACKAGE_FILENAME = process.env.HOT_FILENAME;
+
+module.exports.create = function create(envArgs) {
+  const configBase = configFactory.create(envArgs);
+  const configFull = configFactory.create(envArgs);
+
+  configBase.forEach(function(c) {
+    c.output.filename = PACKAGE_FILENAME + '.js';
+
+    c.devtool = 'source-map';
+    // Exclude all external dependencies from 'base' bundle (handsontable.js and handsontable.css files)
+    c.externals = {
+      numbro: {
+        root: 'numbro',
+        commonjs2: 'numbro',
+        commonjs: 'numbro',
+        amd: 'numbro',
+      },
+      moment: {
+        root: 'moment',
+        commonjs2: 'moment',
+        commonjs: 'moment',
+        amd: 'moment',
+      },
+      pikaday: {
+        root: 'Pikaday',
+        commonjs2: 'pikaday',
+        commonjs: 'pikaday',
+        amd: 'pikaday',
+      }
+    };
+    c.module.rules.unshift({
+      test: [
+        // Disable loading css files from pikaday module
+        /pikaday\/css/,
+      ],
+      loader: path.resolve(__dirname, 'loader/empty-loader.js'),
+    });
+    c.plugins.push(
+      new ExtractTextPlugin(PACKAGE_FILENAME + '.css')
+    );
+  });
+
+  configFull.forEach(function(c) {
+    c.output.filename = PACKAGE_FILENAME + '.full.js';
+    c.module.rules.unshift({
+      test: /numbro/,
+      use: [
+        {
+          loader: path.resolve(__dirname, 'loader/exports-to-window-loader.js'),
+          options: {
+            numbro: 'numbro',
+          }
+        }
+      ]
+    });
+    c.module.rules.unshift({
+      test: /moment/,
+      use: [
+        {
+          loader: path.resolve(__dirname, 'loader/exports-to-window-loader.js'),
+          options: {
+            moment: 'moment',
+          }
+        }
+      ]
+    });
+
+    c.plugins.push(
+      new ExtractTextPlugin(PACKAGE_FILENAME + '.full.css')
+    );
+  });
+
+  return [].concat(configBase, configFull);
+}

+ 109 - 0
handsontable/.config/languages-development.js

@@ -0,0 +1,109 @@
+'use strict';
+
+/**
+ * Config responsible for building not minified Handsontable `languages/` files.
+ */
+const NEW_LINE_CHAR = '\n';
+const SOURCE_LANGUAGES_DIRECTORY = 'src/i18n/languages';
+const OUTPUT_LANGUAGES_DIRECTORY = 'languages';
+
+const path = require('path');
+const StringReplacePlugin  = require('string-replace-webpack-plugin');
+const WebpackOnBuildPlugin = require('on-build-webpack');
+const fs  = require('fs');
+const fsExtra  = require('fs-extra');
+
+function getEntryJsFiles() {
+  const entryObject = {};
+  const filesInLanguagesDirectory = fs.readdirSync(SOURCE_LANGUAGES_DIRECTORY);
+
+  filesInLanguagesDirectory.forEach((fileName) => {
+    const jsExtensionRegExp = /\.js$/;
+
+    if (jsExtensionRegExp.test(fileName)) {
+      let fileNameWithoutExtension = fileName.replace(jsExtensionRegExp, '');
+
+      if (fileNameWithoutExtension === 'index') {
+        fileNameWithoutExtension = 'all';
+      }
+
+      entryObject[fileNameWithoutExtension] = path.resolve(SOURCE_LANGUAGES_DIRECTORY, fileName);
+    }
+  });
+
+  return entryObject;
+}
+
+const ruleForSnippetsInjection = {
+  test: /\.js$/,
+  loader: StringReplacePlugin.replace({
+    replacements: [
+      {
+        pattern: /import.+constants.+/,
+        replacement: function() {
+          const snippet1 = `import Handsontable from '../../handsontable';`;
+          const snippet2 = `const C = Handsontable.languages.dictionaryKeys;`;
+
+          return `${snippet1}${NEW_LINE_CHAR.repeat(2)}${snippet2}`;
+        }
+      },
+      {
+        pattern: /export default dictionary.+/,
+        replacement: function(matchingPhrase) {
+          const snippet = `Handsontable.languages.registerLanguageDictionary(dictionary);`;
+
+          return `${snippet}${NEW_LINE_CHAR.repeat(2)}${matchingPhrase}`;
+        }
+      }
+    ]
+  })
+};
+
+module.exports.create = function create() {
+  const config = {
+    entry: getEntryJsFiles(),
+    output: {
+      path: path.resolve(__dirname, '../' + OUTPUT_LANGUAGES_DIRECTORY),
+      libraryTarget: 'umd',
+      filename: '[name].js',
+      // Workaround below: Without this option webpack would export all language packs as globals
+      libraryExport: '___',
+      umdNamedDefine: true
+    },
+    externals: {
+      '../../handsontable': {
+        root: 'Handsontable',
+        commonjs2: '../../handsontable',
+        commonjs: '../../handsontable',
+        amd: '../../handsontable',
+      },
+    },
+    module: {
+      rules: [
+        {test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'},
+        ruleForSnippetsInjection
+      ]
+    },
+    plugins: [
+      new WebpackOnBuildPlugin(() => {
+        const filesInOutputLanguagesDirectory = fs.readdirSync(OUTPUT_LANGUAGES_DIRECTORY);
+        const indexFileName = 'index.js';
+        const allLanguagesFileName = 'all.js';
+
+        // copy files from `languages` directory to `dist/languages` directory
+        filesInOutputLanguagesDirectory.forEach((fileName) => {
+          if (fileName !== indexFileName) {
+            fsExtra.copySync(`${OUTPUT_LANGUAGES_DIRECTORY}/${fileName}`, `dist/languages/${fileName}`);
+          }
+        });
+
+        // copy from `languages/all.js` to `languages/index.js`
+        if (filesInOutputLanguagesDirectory.includes(allLanguagesFileName)) {
+          fsExtra.copySync(`${OUTPUT_LANGUAGES_DIRECTORY}/${allLanguagesFileName}`, `${OUTPUT_LANGUAGES_DIRECTORY}/${indexFileName}`);
+        }
+      })
+    ]
+  };
+
+  return [config];
+};

+ 40 - 0
handsontable/.config/languages-production.js

@@ -0,0 +1,40 @@
+'use strict';
+
+/**
+ * Config responsible for building minified Handsontable `dist/languages/` files.
+ */
+const path = require('path');
+const webpack  = require('webpack');
+const configFactory = require('./languages-development');
+const OUTPUT_LANGUAGES_DIRECTORY = 'dist/languages';
+
+module.exports.create = function create() {
+  const configs = configFactory.create();
+
+  // Add uglifyJs plugin for each configuration
+  configs.forEach(function(config) {
+    config.output.path = path.resolve(__dirname, '../', OUTPUT_LANGUAGES_DIRECTORY);
+    config.output.filename = '[name].min.js';
+
+    config.plugins = [
+      new webpack.optimize.UglifyJsPlugin({
+        compressor: {
+          pure_getters: true,
+          unsafe: true,
+          unsafe_comps: true,
+          warnings: false,
+          screw_ie8: true,
+        },
+        mangle: {
+          screw_ie8: true,
+        },
+        output: {
+          comments: /^!|@preserve|@license|@cc_on/i,
+          screw_ie8: true,
+        },
+      })
+    ];
+  });
+
+  return [].concat(configs);
+};

+ 11 - 0
handsontable/.config/loader/empty-loader.js

@@ -0,0 +1,11 @@
+module.exports = function() {
+	this.cacheable();
+
+	return '';
+};
+
+module.exports.pitch = function() {
+	this.cacheable();
+
+	return '';
+};

+ 25 - 0
handsontable/.config/loader/exports-to-window-loader.js

@@ -0,0 +1,25 @@
+var loaderUtils = require('loader-utils');
+var FOOTER = '/*** EXPORTS FROM exports-to-window-loader ***/\n';
+var alreadyExported = {};
+
+module.exports = function(content, sourceMap) {
+	if (this.cacheable) {
+    this.cacheable();
+  }
+	var query = loaderUtils.getOptions(this) || {};
+	var exports = [];
+	var keys = Object.keys(query);
+
+  keys.forEach(function(key) {
+    if (!alreadyExported[key]) {
+      alreadyExported[key] = true;
+      exports.push("window['" + key + "'] = require('" + query[key] + "');");
+    }
+  });
+
+  if (exports.length) {
+    content = content + '\n\n' + FOOTER + exports.join('\n');
+  }
+
+  return content;
+}

+ 37 - 0
handsontable/.config/plugin/jasmine-html.js

@@ -0,0 +1,37 @@
+var util = require('util');
+var path = require('path');
+var fs = require('fs');
+var HtmlWebpackPlugin = require('html-webpack-plugin');
+var jasmineCore = require('jasmine-core');
+
+var jasmineFiles = jasmineCore.files;
+var jasminePath = toRelativePath(jasmineFiles.path);
+var jasmineBootDir = toRelativePath(jasmineFiles.bootDir);
+
+function JasmineWebpackPlugin(options) {
+  options = options || {};
+
+  return new HtmlWebpackPlugin({
+    inject: true,
+    filename: options.filename || 'SpecRunner.html',
+    template: path.join(__dirname, 'template.ejs'),
+    baseJasminePath: options.baseJasminePath || '',
+    jasmineJsFiles: toRelativeFiles(jasminePath, jasmineFiles.jsFiles).concat(toRelativeFiles(jasmineBootDir, jasmineFiles.bootFiles)),
+    jasmineCssFiles: toRelativeFiles(jasminePath, jasmineFiles.cssFiles),
+    externalJsFiles: options.externalJsFiles || [],
+    externalCssFiles: options.externalCssFiles || [],
+    minify: false,
+  });
+}
+
+function toRelativePath(dirname) {
+  return dirname.replace(process.cwd(), '').replace(/^\//, '');
+}
+
+function toRelativeFiles(dirname, files) {
+  return files.map(function(file) {
+    return path.join(dirname, file);
+  });
+}
+
+module.exports = JasmineWebpackPlugin;

+ 22 - 0
handsontable/.config/plugin/template.ejs

@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Jasmine Spec Runner</title>
+  <% for (var i = 0; i < htmlWebpackPlugin.options.jasmineCssFiles.length; i++) { %>
+    <link rel="stylesheet" href="<%= htmlWebpackPlugin.options.baseJasminePath %><%= htmlWebpackPlugin.options.jasmineCssFiles[i] %>">
+  <% } %>
+  <% for (var i = 0; i < htmlWebpackPlugin.options.externalCssFiles.length; i++) { %>
+    <link rel="stylesheet" href="<%= htmlWebpackPlugin.options.externalCssFiles[i] %>">
+  <% } %>
+  <% for (var i = 0; i < htmlWebpackPlugin.options.jasmineJsFiles.length; i++) { %>
+    <script src="<%= htmlWebpackPlugin.options.baseJasminePath %><%= htmlWebpackPlugin.options.jasmineJsFiles[i] %>"></script>
+  <% } %>
+  <% for (var i = 0; i < htmlWebpackPlugin.options.externalJsFiles.length; i++) { %>
+    <script src="<%= htmlWebpackPlugin.options.externalJsFiles[i] %>"></script>
+  <% } %>
+</head>
+<body>
+
+</body>
+</html>

+ 86 - 0
handsontable/.config/production.js

@@ -0,0 +1,86 @@
+'use strict';
+
+/**
+ * Config responsible for building Handsontable `dist/` minified files:
+ *  - handsontable.min.js
+ *  - handsontable.min.css
+ *  - handsontable.full.min.js
+ *  - handsontable.full.min.css
+ */
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
+const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
+const webpack = require('webpack');
+const configFactory = require('./development');
+
+const PACKAGE_FILENAME = process.env.HOT_FILENAME;
+
+module.exports.create = function create(envArgs) {
+  const config = configFactory.create(envArgs);
+
+  // Add uglifyJs plugin for each configuration
+  config.forEach(function(c) {
+    const isFullBuild = /\.full\.js$/.test(c.output.filename);
+
+    c.devtool = false;
+    c.output.filename = c.output.filename.replace(/\.js$/, '.min.js');
+
+    // Remove all 'ExtractTextPlugin' instances
+    c.plugins = c.plugins.filter(function(plugin) {
+      return !(plugin instanceof ExtractTextPlugin);
+    });
+
+    c.plugins.push(
+      new UglifyJSPlugin({
+        uglifyOptions: {
+          compressor: {
+            pure_getters: true,
+            unsafe: true,
+            unsafe_comps: true,
+            warnings: false,
+          },
+          mangle: true,
+          output: {
+            comments: /^!|@preserve|@license|@cc_on/i,
+          },
+        }
+      }),
+      new ExtractTextPlugin(PACKAGE_FILENAME + (isFullBuild ? '.full' : '') + '.min.css'),
+      new OptimizeCssAssetsPlugin({
+        assetNameRegExp: isFullBuild ? /\.full\.min\.css$/ : /\.min\.css$/,
+        cssProcessorOptions: { zindex: false },
+      })
+    );
+
+    if (isFullBuild) {
+      c.plugins.push(
+        new CopyWebpackPlugin([
+          { // moment
+            from: {glob: 'node_modules/moment/@(moment.js|LICENSE)'}, to: 'moment', flatten: true
+          },
+          {
+            from: {glob: 'node_modules/moment/locale/*.js'}, to: 'moment/locale', flatten: true
+          },
+          { // numbro
+            from: {glob: 'node_modules/numbro/@(LICENSE-Numeraljs|LICENSE)'}, to: 'numbro', flatten: true
+          },
+          {
+            from: {glob: 'node_modules/numbro/dist/@(numbro.js|languages.min.js)'}, to: 'numbro', flatten: true
+          },
+          {
+            from: {glob: 'node_modules/numbro/dist/languages/*.js'}, to: 'numbro/languages', flatten: true
+          },
+          { // pikaday
+            from: {glob: 'node_modules/pikaday/@(LICENSE|pikaday.js)'}, to: 'pikaday', flatten: true
+          },
+          {
+            from: {glob: 'node_modules/pikaday/css/pikaday.css'}, to: 'pikaday', flatten: true
+          },
+        ])
+      );
+    }
+  });
+
+  return config;
+}

+ 67 - 0
handsontable/.config/test-e2e.js

@@ -0,0 +1,67 @@
+/**
+ * Config responsible for building End-to-End test files (bundled into `test/dist/`):
+ *  - e2e.entry.js
+ *  - helpers.entry.js
+ */
+const path = require('path');
+const webpack = require('webpack');
+const configFactory = require('./base');
+const JasmineHtml = require('./plugin/jasmine-html');
+
+module.exports.create = function create(envArgs) {
+  const config = configFactory.create(envArgs);
+
+  config.forEach(function(c) {
+    c.devtool = 'cheap-module-source-map';
+    c.target = 'web';
+    c.output = {
+      libraryTarget: 'var',
+      filename: '[name].entry.js',
+      path: path.resolve(__dirname, '../test/dist'),
+    };
+    c.resolve.alias.handsontable = path.resolve(__dirname, '../src');
+
+    c.module.rules.unshift({
+      test: [
+         // Disable loading css files from pikaday module
+        /pikaday\/css/,
+      ],
+      loader: path.resolve(__dirname, 'loader/empty-loader.js'),
+    });
+
+    c.externals = [
+      {
+        window: 'window',
+      },
+    ];
+
+    c.plugins.push(
+      new JasmineHtml({
+        filename: path.resolve(__dirname, '../test/E2ERunner.html'),
+        baseJasminePath: '../',
+        externalCssFiles: [
+          'lib/normalize.css',
+          '../dist/handsontable.css',
+          'helpers/common.css',
+        ],
+        externalJsFiles: [
+          'helpers/jasmine-bridge-reporter.js',
+          'lib/jquery.min.js',
+          'lib/jquery.simulate.js',
+          'lib/lodash.underscore.js',
+          'lib/backbone.js',
+          '../node_modules/numbro/dist/numbro.js',
+          '../node_modules/numbro/dist/languages.min.js',
+          '../dist/moment/moment.js',
+          '../dist/pikaday/pikaday.js',
+          '../dist/handsontable.js',
+          '../dist/languages/all.js',
+        ],
+      })
+    );
+
+    c.node.global = true;
+  });
+
+  return [].concat(config);
+}

+ 48 - 0
handsontable/.config/test-mobile.js

@@ -0,0 +1,48 @@
+/**
+ * Config responsible for building Mobile End-to-End test files (bundled into `test/dist/`):
+ *  - mobile.entry.js
+ *  - helpers.entry.js
+ */
+const path = require('path');
+const webpack = require('webpack');
+const configFactory = require('./test-e2e');
+const JasmineHtml = require('./plugin/jasmine-html');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+module.exports.create = function create(envArgs) {
+  const config = configFactory.create(envArgs);
+
+  config.forEach(function(c) {
+
+    // Remove all 'JasmineHtml' instances
+    c.plugins = c.plugins.filter(function(plugin) {
+      return !(plugin instanceof HtmlWebpackPlugin);
+    });
+
+    c.plugins.push(
+      new JasmineHtml({
+        filename: path.resolve(__dirname, '../test/MobileRunner.html'),
+        baseJasminePath: '../',
+        externalCssFiles: [
+          'lib/normalize.css',
+          '../dist/handsontable.css',
+          'helpers/common.css',
+        ],
+        externalJsFiles: [
+          'lib/jquery.min.js',
+          'lib/jquery.simulate.js',
+          'lib/lodash.underscore.js',
+          'lib/backbone.js',
+          '../node_modules/numbro/dist/numbro.js',
+          '../node_modules/numbro/dist/languages.min.js',
+          '../dist/moment/moment.js',
+          '../dist/pikaday/pikaday.js',
+          '../dist/handsontable.js',
+          '../dist/languages/all.js',
+        ],
+      })
+    );
+  });
+
+  return [].concat(config);
+}

+ 47 - 0
handsontable/.config/test-production.js

@@ -0,0 +1,47 @@
+'use strict';
+
+/**
+ * Config responsible for building End-to-End test files (bundled into `test/dist/`). These tests testing `*.full.min.js` files:
+ *  - e2e.entry.js
+ *  - helpers.entry.js
+ */
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const path = require('path');
+const webpack = require('webpack');
+const configFactory = require('./test-e2e');
+const JasmineHtml = require('./plugin/jasmine-html');
+
+module.exports.create = function create(envArgs) {
+  const config = configFactory.create(envArgs);
+
+  config.forEach(function(c) {
+    // Remove all 'JasmineHtml' instances
+    c.plugins = c.plugins.filter(function(plugin) {
+      return !(plugin instanceof HtmlWebpackPlugin);
+    });
+
+    c.plugins.push(
+      new JasmineHtml({
+        filename: path.resolve(__dirname, '../test/E2ERunner.html'),
+        baseJasminePath: '../',
+        externalCssFiles: [
+          'lib/normalize.css',
+          '../dist/handsontable.full.min.css',
+          'helpers/common.css',
+        ],
+        externalJsFiles: [
+          'helpers/jasmine-bridge-reporter.js',
+          'lib/jquery.min.js',
+          'lib/jquery.simulate.js',
+          'lib/lodash.underscore.js',
+          'lib/backbone.js',
+          '../dist/handsontable.full.min.js',
+          '../node_modules/numbro/dist/languages.min.js',
+          '../dist/languages/all.min.js',
+        ],
+      })
+    );
+  });
+
+  return [].concat(config);
+}

+ 62 - 0
handsontable/.config/test-walkontable.js

@@ -0,0 +1,62 @@
+'use strict';
+
+/**
+ * Config responsible for building Walkontable test files (bundled into `src/3rdparty/walkontable/test/dist/`):
+ *  - specs.entry.js
+ *  - helpers.entry.js
+ */
+const path = require('path');
+const webpack = require('webpack');
+const JasmineHtml = require('./plugin/jasmine-html');
+
+const wotPath = path.resolve(__dirname, '../src/3rdparty/walkontable');
+
+module.exports.create = function create(envArgs) {
+  const config = {
+    target: 'web',
+    devtool: 'cheap-module-source-map',
+    output: {
+      libraryTarget: 'var',
+      filename: '[name].entry.js',
+      path: path.resolve(wotPath, 'test/dist'),
+    },
+    module: {
+      rules: [
+        {
+          test: /\.js$/,
+          loader: 'babel-loader',
+          exclude: [
+            /node_modules/,
+          ],
+          options: {
+            cacheDirectory: true,
+          },
+        }
+      ]
+    },
+    externals: [
+      {
+        window: 'window',
+      },
+    ],
+    plugins: [
+      // This helps ensure the builds are consistent if source code hasn't changed
+      new webpack.optimize.OccurrenceOrderPlugin(),
+      new JasmineHtml({
+        filename: path.resolve(wotPath, 'test/SpecRunner.html'),
+        baseJasminePath: '../../../../',
+        externalCssFiles: [
+          '../css/walkontable.css',
+        ],
+        externalJsFiles: [
+          'helpers/jasmine-bridge-reporter.js',
+          'lib/jquery.min.js',
+          'lib/jquery.simulate.js',
+          '../dist/walkontable.js',
+        ],
+      })
+    ],
+  };
+
+  return [].concat(config);
+}

+ 44 - 0
handsontable/.config/walkontable.js

@@ -0,0 +1,44 @@
+'use strict';
+
+/**
+ * Config responsible for building Walkontable (bundled into `src/3rdparty/walkontable/dist/`):
+ *  - walkontable.js
+ */
+const path = require('path');
+const webpack = require('webpack');
+const configFactory = require('./base');
+const JasmineHtml = require('./plugin/jasmine-html');
+
+const wotPath = path.resolve(__dirname, '../src/3rdparty/walkontable');
+
+module.exports.create = function create(envArgs) {
+  const config = {
+    devtool: 'cheap-module-source-map',
+    output: {
+      library: 'Walkontable',
+      libraryTarget: 'var',
+      filename: 'walkontable.js',
+      path: path.resolve(wotPath, 'dist'),
+    },
+    module: {
+      rules: [
+        {
+          test: /\.js$/,
+          loader: 'babel-loader',
+          exclude: [
+            /node_modules/,
+          ],
+          options: {
+            cacheDirectory: true,
+          },
+        }
+      ]
+    },
+    plugins: [
+      // This helps ensure the builds are consistent if source code hasn't changed
+      new webpack.optimize.OccurrenceOrderPlugin(),
+    ],
+  };
+
+  return [].concat(config);
+}

+ 55 - 0
handsontable/.config/watch.js

@@ -0,0 +1,55 @@
+'use strict';
+
+/**
+ * Config responsible for building Handsontable `dist/` files with enabled watching mode:
+ *  - handsontable.js
+ *  - handsontable.css
+ */
+const path = require('path');
+const webpack = require('webpack');
+const configFactory = require('./base');
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+const PACKAGE_FILENAME = process.env.HOT_FILENAME;
+
+module.exports.create = function create(envArgs) {
+  const config = configFactory.create(envArgs);
+
+  config.forEach(function(c) {
+    c.devtool = 'cheap-module-source-map';
+    c.output.filename = PACKAGE_FILENAME + '.js';
+    // Exclude all external dependencies from 'base' bundle (handsontable.js and handsontable.css)
+    c.externals = {
+      numbro: {
+        root: 'numbro',
+        commonjs2: 'numbro',
+        commonjs: 'numbro',
+        amd: 'numbro',
+      },
+      moment: {
+        root: 'moment',
+        commonjs2: 'moment',
+        commonjs: 'moment',
+        amd: 'moment',
+      },
+      pikaday: {
+        root: 'Pikaday',
+        commonjs2: 'pikaday',
+        commonjs: 'pikaday',
+        amd: 'pikaday',
+      }
+    };
+    c.module.rules.unshift({
+      test: [
+         // Disable loading css files from pikaday module
+        /pikaday\/css/,
+      ],
+      loader: path.resolve(__dirname, 'loader/empty-loader.js'),
+    });
+    c.plugins.push(
+      new ExtractTextPlugin(PACKAGE_FILENAME + '.css')
+    );
+  });
+
+  return config;
+}

+ 17 - 0
handsontable/.editorconfig

@@ -0,0 +1,17 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+[*]
+
+# Change these settings to your own preference
+indent_style = space
+indent_size = 2
+
+# We recommend you to keep these unchanged
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = false
+insert_final_newline = true

+ 12 - 0
handsontable/.eslintignore

@@ -0,0 +1,12 @@
+node_modules
+src/3rdparty/walkontable/test/lib/*
+Gruntfile.js
+dist/*
+commonjs/*
+languages/*
+es/*
+lib/*
+src/3rdparty/walkontable/test/dist/*
+src/3rdparty/walkontable/dist/*
+test/*
+src/plugins/*/test/*

+ 95 - 0
handsontable/.eslintrc.js

@@ -0,0 +1,95 @@
+module.exports = {
+  parser: '@typescript-eslint/parser',
+  plugins: [
+    '@typescript-eslint',
+  ],
+  extends: [
+    'airbnb-base',
+    'plugin:@typescript-eslint/eslint-recommended',
+    'plugin:@typescript-eslint/recommended',
+  ],
+  env: {
+    browser: true,
+    commonjs: true,
+    jasmine: true,
+    jest: true,
+    es6: true,
+  },
+  rules: {
+    'arrow-parens': [
+      'error',
+      'as-needed',
+      { requireForBlockBody: true }
+    ],
+    'class-methods-use-this': 'off',
+    'comma-dangle': 'off',
+    'consistent-return': 'off',
+    'func-names': 'off',
+    'import/no-extraneous-dependencies': 'off',
+    'no-use-before-define': 'off',
+    indent: [
+      'error',
+      2,
+      {
+        SwitchCase: 1,
+        FunctionDeclaration: { parameters: 'first' },
+        FunctionExpression: { parameters: 'first' }
+      }
+    ],
+    'newline-per-chained-call': 'off',
+    'no-constant-condition': [
+      'error',
+      { checkLoops: false }
+    ],
+    'no-eq-null': 'error',
+    'no-mixed-operators': [
+      'error',
+      { groups: [['+', '-', '*', '/', '%', '**']] }
+    ],
+    'no-multiple-empty-lines': [
+      'error',
+      { max: 1 }
+    ],
+    'no-param-reassign': 'off',
+    'no-shadow': 'off',
+    '@typescript-eslint/no-empty-interface': 'off',
+    '@typescript-eslint/no-var-requires': 'off',
+    'import/no-named-as-default': 'off',
+    'import/no-named-as-default-member': 'off',
+    '@typescript-eslint/no-this-alias': 'off',
+    '@typescript-eslint/no-empty-function': 'off',
+    'no-plusplus': [
+      'error',
+      { allowForLoopAfterthoughts: true }
+    ],
+    'no-restricted-globals': [
+      'error',
+      'Handsontable',
+      {
+        name: 'console',
+        message: 'Using the `console` object is not allowed within Handsontable. Please use one of the helpers from the `console.js` file instead.'
+      }
+    ],
+    'no-underscore-dangle': 'off',
+    '@typescript-eslint/ban-types': 'off',
+    'max-len': 'off',
+    'no-void': 'off',
+    'padded-blocks': 'off',
+    quotes: ['error', 'single'],
+    'space-before-function-paren': ['error', 'never'],
+  },
+  overrides: [
+    {
+      files: ['test/**', 'src/3rdparty/walkontable/test/**', '*.unit.js', '*.e2e.js'],
+      rules: {
+        'import/extensions': 'off',
+        'import/no-unresolved': [
+          'error',
+          { ignore: ['handsontable', 'walkontable'] }
+        ],
+        'no-restricted-globals': 'off',
+        'no-undef': 'off',
+      }
+    }
+  ],
+};

+ 2 - 0
handsontable/.gitattributes

@@ -0,0 +1,2 @@
+# Set default behaviour, in case users don't have core.autocrlf set.
+* text=crlf

+ 21 - 0
handsontable/.gitignore

@@ -0,0 +1,21 @@
+.DS_Store
+.grunt
+.idea
+.vscode
+_SpecRunner.html
+test/E2ERunner.html
+test/UnitRunner.html
+test/MobileRunner.html
+test/dist/
+src/3rdparty/walkontable/test/dist/
+src/3rdparty/walkontable/dist/
+cars.sqlite
+dev*.html
+npm-debug.log
+
+!dist/README.md
+dist/
+node_modules/
+commonjs/
+es/
+languages/

+ 20 - 0
handsontable/.npmignore

@@ -0,0 +1,20 @@
+.DS_Store
+.grunt
+.config
+.github
+.idea
+_SpecRunner.html
+cars.sqlite
+dev.html
+webpack.config.js
+
+!dist/README.md
+test/
+node_modules/
+
+# this directories are necessary for build PRO package
+#demo/
+#src/
+#lib/
+#test/
+#tasks/

+ 20 - 0
handsontable/.travis.yml

@@ -0,0 +1,20 @@
+language: node_js
+
+sudo: false
+
+node_js:
+  - '8'
+
+before_script:
+  - export TZ=Europe/Warsaw
+  - chmod ugo+x test/scripts/trigger-hot-builder-tests.sh
+  - chmod ugo+x test/scripts/trigger-pro-tests.sh
+
+after_success:
+  - "./test/scripts/trigger-hot-builder-tests.sh"
+  - "./test/scripts/trigger-pro-tests.sh"
+
+notifications:
+  email: false
+  slack:
+    secure: LDlPC3IbXLbve4yq6kNznoIR5WgMhS2x+/KomgbVRDkUpySzyt27dUtRJqgkAqtkHhDhd0FbpROZNPriQVJI8nLLux0xdl9dngy6Lazl2qVzShrtdN01I3lVxysJlDM3phKHQCPmGhd/1v1qjfTAu45tM9pnYACSnDfdfh46b84=

+ 1 - 0
handsontable/CHANGELOG.md

@@ -0,0 +1 @@
+All releases are described at https://github.com/handsontable/handsontable/releases

+ 1 - 0
handsontable/CNAME

@@ -0,0 +1 @@
+handsontable.com

+ 46 - 0
handsontable/CODE_OF_CONDUCT.md

@@ -0,0 +1,46 @@
+# Handsontable Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@handsontable.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/

+ 17 - 0
handsontable/CONTRIBUTING.md

@@ -0,0 +1,17 @@
+# Contributing to Handsontable
+
+Your contributions to this project are very welcome. If you want to fix a bug or propose a new feature, you can open a new Pull Request but first make sure it follows these general rules:
+
+1. Sign this [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2) to allow us to publish your changes to the code.
+2. Make your changes on a separate branch. This will speed up the merging process.
+3. Always make the target of your pull request the `develop` branch, not `master`.
+4. Do not edit files in `languages/` (e.g: `index.js`, `all.js`, `en-US.js`) and `dist/` (e.g: `handsontable.js`, `handsontable.css`, `handsontable.full.js`, `handsontable.full.css`, `all.js`, `en-US.js`) directories. Instead, edit files inside the `src/` directory and then use `npm run build` to make a build. More information about this you can find [here](https://docs.handsontable.com/tutorial-custom-build.html).
+5. **Important: For any change you make, please add at least one test case** in `test/e2e/` (for End-to-End tests), `test/unit/` or `src/3rdparty/walkontable/test/spec/`. That will help us understand the issue and make sure that it stays fixed forever. Read more about our [testing process](http://docs.handsontable.com/tutorial-testing.html).
+6. Please lint the code, i.e. by `npm run lint` task. It should follow [our coding style](https://github.com/handsontable/handsontable/blob/master/.eslintrc), inspired by [Airbnb JavaScript Style](https://github.com/airbnb/javascript).
+7. Add a thorough description of all the changes.
+
+Thank you for your commitment!
+
+## Team rules
+
+The Handsontable team utilizes Git-Flow. Read more - [How we use Git-Flow](https://github.com/handsontable/handsontable/wiki/How-we-use-Git-Flow)

+ 23 - 0
handsontable/LICENSE

@@ -0,0 +1,23 @@
+(The MIT License)
+
+Copyright (c) 2012-2014 Marcin Warpechowski
+Copyright (c) 2015 Handsoncode sp. z o.o. <hello@handsoncode.net>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Fichier diff supprimé car celui-ci est trop grand
+ 172 - 0
handsontable/README.md


+ 174 - 0
handsontable/babel.config.js

@@ -0,0 +1,174 @@
+module.exports = {
+  presets: [
+    [
+      '@babel/preset-env', {
+        targets: {
+          chrome: '41',
+          firefox: '34',
+          ie: '9',
+          safari: '9',
+          browsers: ['last 3 versions']
+        },
+        exclude: [
+          'transform-regenerator',
+          'es6.typed.array-buffer',
+          'es6.typed.data-view',
+          'es6.typed.int8-array',
+          'es6.typed.uint8-array',
+          'es6.typed.uint8-clamped-array',
+          'es6.typed.int16-array',
+          'es6.typed.uint16-array',
+          'es6.typed.int32-array',
+          'es6.typed.uint32-array',
+          'es6.typed.float32-array',
+          'es6.typed.float64-array',
+          'es6.reflect.apply',
+          'es6.reflect.construct',
+          'es6.reflect.define-property',
+          'es6.reflect.delete-property',
+          'es6.reflect.get',
+          'es6.reflect.get-own-property-descriptor',
+          'es6.reflect.get-prototype-of',
+          'es6.reflect.has',
+          'es6.reflect.is-extensible',
+          'es6.reflect.own-keys',
+          'es6.reflect.prevent-extensions',
+          'es6.reflect.set',
+          'es6.reflect.set-prototype-of',
+          'es6.math.acosh',
+          'es6.math.acosh',
+          'es6.math.asinh',
+          'es6.math.atanh',
+          'es6.math.cbrt',
+          'es6.math.clz32',
+          'es6.math.cosh',
+          'es6.math.expm1',
+          'es6.math.fround',
+          'es6.math.hypot',
+          'es6.math.imul',
+          'es6.math.log1p',
+          'es6.math.log10',
+          'es6.math.log2',
+          'es6.math.sign',
+          'es6.math.sinh',
+          'es6.math.tanh',
+          'es6.math.tanh',
+          'es6.math.trunc',
+          'web.timers'
+        ],
+        modules: false,
+        debug: false,
+        useBuiltIns: 'entry'
+      }]
+  ],
+  plugins: [
+    ['@babel/plugin-proposal-object-rest-spread', { useBuiltIns: true }],
+    ['transform-inline-environment-variables']
+  ],
+  env: {
+    commonjs: {
+      plugins: [
+        ['@babel/plugin-transform-modules-commonjs', { loose: true }]
+      ]
+    },
+    commonjs_dist: {
+      plugins: [
+        ['@babel/plugin-transform-modules-commonjs', { loose: true }],
+        ['babel-plugin-transform-require-ignore', { extensions: ['.css'] }]
+      ]
+    },
+    commonjs_e2e: {
+      presets: [
+        [
+          '@babel/preset-env', {
+            targets: {
+              chrome: '41',
+              firefox: '34',
+              ie: '9',
+              safari: '9',
+              browsers: ['last 3 versions']
+            },
+            exclude: [
+              'transform-regenerator',
+              'es6.typed.array-buffer',
+              'es6.typed.data-view',
+              'es6.typed.int8-array',
+              'es6.typed.uint8-array',
+              'es6.typed.uint8-clamped-array',
+              'es6.typed.int16-array',
+              'es6.typed.uint16-array',
+              'es6.typed.int32-array',
+              'es6.typed.uint32-array',
+              'es6.typed.float32-array',
+              'es6.typed.float64-array',
+              'es6.reflect.apply',
+              'es6.reflect.construct',
+              'es6.reflect.define-property',
+              'es6.reflect.delete-property',
+              'es6.reflect.get',
+              'es6.reflect.get-own-property-descriptor',
+              'es6.reflect.get-prototype-of',
+              'es6.reflect.has',
+              'es6.reflect.is-extensible',
+              'es6.reflect.own-keys',
+              'es6.reflect.prevent-extensions',
+              'es6.reflect.set',
+              'es6.reflect.set-prototype-of',
+              'es6.math.acosh',
+              'es6.math.acosh',
+              'es6.math.asinh',
+              'es6.math.atanh',
+              'es6.math.cbrt',
+              'es6.math.clz32',
+              'es6.math.cosh',
+              'es6.math.expm1',
+              'es6.math.fround',
+              'es6.math.hypot',
+              'es6.math.imul',
+              'es6.math.log1p',
+              'es6.math.log10',
+              'es6.math.log2',
+              'es6.math.sign',
+              'es6.math.sinh',
+              'es6.math.tanh',
+              'es6.math.tanh',
+              'es6.math.trunc',
+              'web.timers'
+            ],
+            modules: false,
+            debug: false,
+            useBuiltIns: 'entry'
+          }]
+      ],
+      plugins: [
+        ['@babel/plugin-transform-modules-commonjs', { loose: true }],
+        [
+          'babel-plugin-forbidden-imports', {
+            allowedModules: [
+              '@babel/polyfill/*',
+              'window',
+              'jasmine-co',
+              'core-js/*',
+              'regenerator-runtime/runtime',
+              './common',
+              './../bootstrap',
+              './helpers/custom-matchers',
+              './asciiTable',
+              './MemoryLeakTest',
+              '../MemoryLeakTest'
+            ]
+          }]
+      ]
+    },
+    es: {
+      plugins: [
+        ['babel-plugin-transform-require-ignore', { extensions: ['.css'] }]
+      ]
+    }
+  },
+
+  ignore: [
+    'src/3rdparty/walkontable/dist/*',
+    'src/3rdparty/walkontable/test/dist/*'
+  ]
+};

+ 44 - 0
handsontable/bower.json

@@ -0,0 +1,44 @@
+{
+  "name": "handsontable",
+  "description": "Spreadsheet-like data grid editor that provides copy/paste functionality compatible with Excel/Google Docs",
+  "main": ["./dist/handsontable.js", "./dist/handsontable.css"],
+  "homepage": "http://handsontable.com/",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/handsontable/handsontable.git"
+  },
+  "authors": [
+    "Handsoncode", "Handsoncode <hello@handsontable.com>"
+  ],
+  "keywords": [
+    "data",
+    "grid",
+    "table",
+    "editor",
+    "grid-editor",
+    "data-grid",
+    "data-table",
+    "spreadsheet",
+    "excel",
+    "tabular-data",
+    "edit-cell",
+    "editable-table",
+    "data-spreadsheet"
+  ],
+  "ignore": [
+    "**/.*",
+    "components",
+    "demo",
+    "node_modules",
+    "src",
+    "test"
+  ],
+  "dependencies": {
+    "moment": "^2.13.0",
+    "numbro": "^2.0.6",
+    "pikaday": "^1.4.0"
+  },
+  "devDependencies": {
+    "chroma-js": "~0.5.6"
+  }
+}

Fichier diff supprimé car celui-ci est trop grand
+ 2778 - 0
handsontable/handsontable.d.ts


+ 37 - 0
handsontable/handsontable.jquery.json

@@ -0,0 +1,37 @@
+{
+  "name": "handsontable",
+  "title": "Handsontable",
+  "version": "6.2.2",
+  "author": {
+    "name": "Handsoncode",
+    "email": "hello@handsontable.com",
+    "url": "http://handsontable.com/"
+  },
+  "licenses": [
+    {
+      "type": "MIT",
+      "url": "https://raw.github.com/handsontable/handsontable/master/LICENSE"
+    }
+  ],
+  "description": "Spreadsheet-like data grid editor that provides copy/paste functionality compatible with Excel/Google Docs",
+  "homepage": "http://handsontable.com/",
+  "docs": "https://github.com/handsontable/handsontable",
+  "demo": "http://handsontable.com/",
+  "download": "https://github.com/handsontable/handsontable/archive/master.zip",
+  "keywords": [
+    "grid",
+    "datagrid",
+    "table",
+    "ui",
+    "input",
+    "ajax",
+    "handsontable",
+    "spreadsheet"
+  ],
+  "bugs": "https://github.com/handsontable/handsontable/issues",
+  "dependencies": {
+    "jquery": ">=1.7",
+    "bootstrap-typeahead.js": "2.1.1",
+    "contextMenu": ">=1.5.25"
+  }
+}

+ 12 - 0
handsontable/hot.config.js

@@ -0,0 +1,12 @@
+const moment = require('moment');
+const packageBody = require('./package.json');
+
+module.exports = {
+  HOT_FILENAME: 'handsontable',
+  HOT_VERSION: packageBody.version,
+  HOT_BASE_VERSION: '',
+  HOT_PACKAGE_TYPE: 'ce',
+  HOT_PACKAGE_NAME: packageBody.name,
+  HOT_BUILD_DATE: moment().format('DD/MM/YYYY HH:mm:ss'),
+  HOT_RELEASE_DATE: '19/12/2018',
+};

+ 119 - 0
handsontable/lib/SheetClip/SheetClip.js

@@ -0,0 +1,119 @@
+/**
+ * SheetClip - Spreadsheet Clipboard Parser
+ * version 0.2
+ *
+ * This tiny library transforms JavaScript arrays to strings that are pasteable by LibreOffice, OpenOffice,
+ * Google Docs and Microsoft Excel.
+ *
+ * Copyright 2012, Marcin Warpechowski
+ * Licensed under the MIT license.
+ * http://github.com/warpech/sheetclip/
+ */
+/*jslint white: true*/
+(function (global) {
+  "use strict";
+
+  function countQuotes(str) {
+    return str.split('"').length - 1;
+  }
+
+  var SheetClip = {
+    /**
+     * Decode spreadsheet string into array
+     *
+     * @param {String} str
+     * @returns {Array}
+     */
+    parse: function (str) {
+      var r, rLen, rows, arr = [], a = 0, c, cLen, multiline, last;
+
+      rows = str.replace(/\r\n|\r/g, '\n').split('\n');
+
+      if (rows.length > 1 && rows[rows.length - 1] === '') {
+        rows.pop();
+      }
+      for (r = 0, rLen = rows.length; r < rLen; r += 1) {
+        rows[r] = rows[r].split('\t');
+
+        for (c = 0, cLen = rows[r].length; c < cLen; c += 1) {
+          if (!arr[a]) {
+            arr[a] = [];
+          }
+          if (multiline && c === 0) {
+            last = arr[a].length - 1;
+            arr[a][last] = arr[a][last] + '\n' + rows[r][0];
+
+            if (multiline && (countQuotes(rows[r][0]) & 1)) { //& 1 is a bitwise way of performing mod 2
+              multiline = false;
+              arr[a][last] = arr[a][last].substring(0, arr[a][last].length - 1).replace(/""/g, '"');
+            }
+          }
+          else {
+            if (c === cLen - 1 && rows[r][c].indexOf('"') === 0 && (countQuotes(rows[r][c]) & 1)) {
+              arr[a].push(rows[r][c].substring(1).replace(/""/g, '"'));
+              multiline = true;
+            }
+            else {
+              arr[a].push(rows[r][c].replace(/""/g, '"'));
+              multiline = false;
+            }
+          }
+        }
+        if (!multiline) {
+          a += 1;
+        }
+      }
+
+      return arr;
+    },
+
+    /**
+     * Encode array into valid spreadsheet string
+     *
+     * @param arr
+     * @returns {String}
+     */
+    stringify: function (arr) {
+      var r, rLen, c, cLen, str = '', val;
+
+      for (r = 0, rLen = arr.length; r < rLen; r += 1) {
+        cLen = arr[r].length;
+
+        for (c = 0; c < cLen; c += 1) {
+          if (c > 0) {
+            str += '\t';
+          }
+          val = arr[r][c];
+
+          if (typeof val === 'string') {
+            if (val.indexOf('\n') > -1) {
+              str += '"' + val.replace(/"/g, '""') + '"';
+            }
+            else {
+              str += val;
+            }
+          }
+          else if (val === null || val === void 0) { // void 0 resolves to undefined
+            str += '';
+          }
+          else {
+            str += val;
+          }
+        }
+
+        if (r !== rLen - 1) {
+          str += '\n';
+        }
+      }
+
+      return str;
+    }
+  };
+
+  if (typeof exports !== 'undefined') {
+    exports.parse = SheetClip.parse;
+    exports.stringify = SheetClip.stringify;
+  } else {
+    global.SheetClip = SheetClip;
+  }
+}(window));

+ 181 - 0
handsontable/lib/autoResize/autoResize.js

@@ -0,0 +1,181 @@
+/**
+ * autoResize - resizes a DOM element to the width and height of another DOM element
+ *
+ * Copyright 2014, Marcin Warpechowski
+ * Licensed under the MIT license
+ */
+
+
+function autoResize() {
+  var defaults = {
+      minHeight: 200,
+      maxHeight: 300,
+      minWidth: 100,
+      maxWidth: 300
+    },
+    el,
+    body = document.body,
+    text = document.createTextNode(''),
+    span = document.createElement('SPAN'),
+    observe = function (element, event, handler) {
+      element.addEventListener(event, handler, false);
+    },
+    unObserve = function (element, event, handler) {
+      element.removeEventListener(event, handler, false);
+    },
+    resize = function (newChar) {
+      var width, scrollHeight;
+
+      if (!newChar) {
+        newChar = "";
+      } else if (!/^[a-zA-Z \.,\\\/\|0-9]$/.test(newChar)) {
+        newChar = ".";
+      }
+
+      if (text.textContent !== void 0) {
+        text.textContent = el.value + newChar;
+      }
+      else {
+        text.data = el.value + newChar; //IE8
+      }
+      span.style.fontSize = getComputedStyle(el).fontSize;
+      span.style.fontFamily = getComputedStyle(el).fontFamily;
+      span.style.whiteSpace = "pre";
+
+      body.appendChild(span);
+      width = span.clientWidth + 2;
+      body.removeChild(span);
+
+      el.style.height = defaults.minHeight + 'px';
+
+      if (defaults.minWidth > width) {
+        el.style.width = defaults.minWidth + 'px';
+
+      } else if (width > defaults.maxWidth) {
+        el.style.width = defaults.maxWidth + 'px';
+
+      } else {
+        el.style.width = width + 'px';
+      }
+      scrollHeight = el.scrollHeight ? el.scrollHeight - 1 : 0;
+
+      if (defaults.minHeight > scrollHeight) {
+        el.style.height = defaults.minHeight + 'px';
+
+      } else if (defaults.maxHeight < scrollHeight) {
+        el.style.height = defaults.maxHeight + 'px';
+        el.style.overflowY = 'visible';
+
+      } else {
+        el.style.height = scrollHeight + 'px';
+      }
+    },
+    delayedResize = function () {
+      window.setTimeout(resize, 0);
+    },
+    extendDefaults = function (config) {
+
+      if (config && config.minHeight) {
+        if (config.minHeight == 'inherit') {
+          defaults.minHeight = el.clientHeight;
+        } else {
+          var minHeight = parseInt(config.minHeight);
+          if (!isNaN(minHeight)) {
+            defaults.minHeight = minHeight;
+          }
+        }
+      }
+
+      if (config && config.maxHeight) {
+        if (config.maxHeight == 'inherit') {
+          defaults.maxHeight = el.clientHeight;
+        } else {
+          var maxHeight = parseInt(config.maxHeight);
+          if (!isNaN(maxHeight)) {
+            defaults.maxHeight = maxHeight;
+          }
+        }
+      }
+
+      if (config && config.minWidth) {
+        if (config.minWidth == 'inherit') {
+          defaults.minWidth = el.clientWidth;
+        } else {
+          var minWidth = parseInt(config.minWidth);
+          if (!isNaN(minWidth)) {
+            defaults.minWidth = minWidth;
+          }
+        }
+      }
+
+      if (config && config.maxWidth) {
+        if (config.maxWidth == 'inherit') {
+          defaults.maxWidth = el.clientWidth;
+        } else {
+          var maxWidth = parseInt(config.maxWidth);
+          if (!isNaN(maxWidth)) {
+            defaults.maxWidth = maxWidth;
+          }
+        }
+      }
+
+      if(!span.firstChild) {
+        span.className = "autoResize";
+        span.style.display = 'inline-block';
+        span.appendChild(text);
+      }
+    },
+    init = function (el_, config, doObserve) {
+      el = el_;
+      extendDefaults(config);
+
+      if (el.nodeName == 'TEXTAREA') {
+        el.style.resize = 'none';
+        el.style.overflowY = '';
+        el.style.height = defaults.minHeight + 'px';
+        el.style.minWidth = defaults.minWidth + 'px';
+        el.style.maxWidth = defaults.maxWidth + 'px';
+        el.style.overflowY = 'hidden';
+      }
+
+      if (doObserve) {
+        observe(el, 'change', resize);
+        observe(el, 'cut', delayedResize);
+        observe(el, 'paste', delayedResize);
+        observe(el, 'drop', delayedResize);
+        observe(el, 'keydown', delayedResize);
+        observe(el, 'focus', resize);
+        observe(el, 'compositionstart', delayedResize);
+        observe(el, 'compositionupdate', delayedResize);
+        observe(el, 'compositionend', delayedResize);
+      }
+
+      resize();
+    };
+
+  function getComputedStyle(element) {
+    return element.currentStyle || document.defaultView.getComputedStyle(element);
+  }
+
+  return {
+    init: function (el_, config, doObserve) {
+      init(el_, config, doObserve);
+    },
+    unObserve: function () {
+      unObserve(el, 'change', resize);
+      unObserve(el, 'cut', delayedResize);
+      unObserve(el, 'paste', delayedResize);
+      unObserve(el, 'drop', delayedResize);
+      unObserve(el, 'keydown', delayedResize);
+      unObserve(el, 'focus', resize);
+      unObserve(el, 'compositionstart', delayedResize);
+      unObserve(el, 'compositionupdate', delayedResize);
+      unObserve(el, 'compositionend', delayedResize);
+    },
+    resize: resize
+  };
+}
+
+if (typeof exports !== 'undefined') {
+  module.exports = autoResize;
+}

+ 631 - 0
handsontable/lib/jsonpatch/json-patch-duplex.js

@@ -0,0 +1,631 @@
+/*!
+ * https://github.com/Starcounter-Jack/JSON-Patch
+ * json-patch-duplex.js version: 0.5.7
+ * (c) 2013 Joachim Wester
+ * MIT license
+ */
+var __extends = (this && this.__extends) || function (d, b) {
+    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+    function __() { this.constructor = d; }
+    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+};
+var OriginalError = Error;
+var jsonpatch;
+(function (jsonpatch) {
+    var _objectKeys = function (obj) {
+        if (_isArray(obj)) {
+            var keys = new Array(obj.length);
+            for (var k = 0; k < keys.length; k++) {
+                keys[k] = "" + k;
+            }
+            return keys;
+        }
+        if (Object.keys) {
+            return Object.keys(obj);
+        }
+        var keys = [];
+        for (var i in obj) {
+            if (obj.hasOwnProperty(i)) {
+                keys.push(i);
+            }
+        }
+        return keys;
+    };
+    function _equals(a, b) {
+        switch (typeof a) {
+            case 'undefined': //backward compatibility, but really I think we should return false
+            case 'boolean':
+            case 'string':
+            case 'number':
+                return a === b;
+            case 'object':
+                if (a === null)
+                    return b === null;
+                if (_isArray(a)) {
+                    if (!_isArray(b) || a.length !== b.length)
+                        return false;
+                    for (var i = 0, l = a.length; i < l; i++)
+                        if (!_equals(a[i], b[i]))
+                            return false;
+                    return true;
+                }
+                var bKeys = _objectKeys(b);
+                var bLength = bKeys.length;
+                if (_objectKeys(a).length !== bLength)
+                    return false;
+                for (var i = 0; i < bLength; i++)
+                    if (!_equals(a[i], b[i]))
+                        return false;
+                return true;
+            default:
+                return false;
+        }
+    }
+    /* We use a Javascript hash to store each
+     function. Each hash entry (property) uses
+     the operation identifiers specified in rfc6902.
+     In this way, we can map each patch operation
+     to its dedicated function in efficient way.
+     */
+    /* The operations applicable to an object */
+    var objOps = {
+        add: function (obj, key) {
+            obj[key] = this.value;
+            return true;
+        },
+        remove: function (obj, key) {
+            delete obj[key];
+            return true;
+        },
+        replace: function (obj, key) {
+            obj[key] = this.value;
+            return true;
+        },
+        move: function (obj, key, tree) {
+            var temp = { op: "_get", path: this.from };
+            apply(tree, [temp]);
+            apply(tree, [
+                { op: "remove", path: this.from }
+            ]);
+            apply(tree, [
+                { op: "add", path: this.path, value: temp.value }
+            ]);
+            return true;
+        },
+        copy: function (obj, key, tree) {
+            var temp = { op: "_get", path: this.from };
+            apply(tree, [temp]);
+            apply(tree, [
+                { op: "add", path: this.path, value: temp.value }
+            ]);
+            return true;
+        },
+        test: function (obj, key) {
+            return _equals(obj[key], this.value);
+        },
+        _get: function (obj, key) {
+            this.value = obj[key];
+        }
+    };
+    /* The operations applicable to an array. Many are the same as for the object */
+    var arrOps = {
+        add: function (arr, i) {
+            arr.splice(i, 0, this.value);
+            return true;
+        },
+        remove: function (arr, i) {
+            arr.splice(i, 1);
+            return true;
+        },
+        replace: function (arr, i) {
+            arr[i] = this.value;
+            return true;
+        },
+        move: objOps.move,
+        copy: objOps.copy,
+        test: objOps.test,
+        _get: objOps._get
+    };
+    /* The operations applicable to object root. Many are the same as for the object */
+    var rootOps = {
+        add: function (obj) {
+            rootOps.remove.call(this, obj);
+            for (var key in this.value) {
+                if (this.value.hasOwnProperty(key)) {
+                    obj[key] = this.value[key];
+                }
+            }
+            return true;
+        },
+        remove: function (obj) {
+            for (var key in obj) {
+                if (obj.hasOwnProperty(key)) {
+                    objOps.remove.call(this, obj, key);
+                }
+            }
+            return true;
+        },
+        replace: function (obj) {
+            apply(obj, [
+                { op: "remove", path: this.path }
+            ]);
+            apply(obj, [
+                { op: "add", path: this.path, value: this.value }
+            ]);
+            return true;
+        },
+        move: objOps.move,
+        copy: objOps.copy,
+        test: function (obj) {
+            return (JSON.stringify(obj) === JSON.stringify(this.value));
+        },
+        _get: function (obj) {
+            this.value = obj;
+        }
+    };
+    var observeOps = {
+        add: function (patches, path) {
+            var patch = {
+                op: "add",
+                path: path + escapePathComponent(this.name),
+                value: this.object[this.name] };
+            patches.push(patch);
+        },
+        'delete': function (patches, path) {
+            var patch = {
+                op: "remove",
+                path: path + escapePathComponent(this.name)
+            };
+            patches.push(patch);
+        },
+        update: function (patches, path) {
+            var patch = {
+                op: "replace",
+                path: path + escapePathComponent(this.name),
+                value: this.object[this.name]
+            };
+            patches.push(patch);
+        }
+    };
+    function escapePathComponent(str) {
+        if (str.indexOf('/') === -1 && str.indexOf('~') === -1)
+            return str;
+        return str.replace(/~/g, '~0').replace(/\//g, '~1');
+    }
+    function _getPathRecursive(root, obj) {
+        var found;
+        for (var key in root) {
+            if (root.hasOwnProperty(key)) {
+                if (root[key] === obj) {
+                    return escapePathComponent(key) + '/';
+                }
+                else if (typeof root[key] === 'object') {
+                    found = _getPathRecursive(root[key], obj);
+                    if (found != '') {
+                        return escapePathComponent(key) + '/' + found;
+                    }
+                }
+            }
+        }
+        return '';
+    }
+    function getPath(root, obj) {
+        if (root === obj) {
+            return '/';
+        }
+        var path = _getPathRecursive(root, obj);
+        if (path === '') {
+            throw new OriginalError("Object not found in root");
+        }
+        return '/' + path;
+    }
+    var beforeDict = [];
+    var Mirror = (function () {
+        function Mirror(obj) {
+            this.observers = [];
+            this.obj = obj;
+        }
+        return Mirror;
+    })();
+    var ObserverInfo = (function () {
+        function ObserverInfo(callback, observer) {
+            this.callback = callback;
+            this.observer = observer;
+        }
+        return ObserverInfo;
+    })();
+    function getMirror(obj) {
+        for (var i = 0, ilen = beforeDict.length; i < ilen; i++) {
+            if (beforeDict[i].obj === obj) {
+                return beforeDict[i];
+            }
+        }
+    }
+    function getObserverFromMirror(mirror, callback) {
+        for (var j = 0, jlen = mirror.observers.length; j < jlen; j++) {
+            if (mirror.observers[j].callback === callback) {
+                return mirror.observers[j].observer;
+            }
+        }
+    }
+    function removeObserverFromMirror(mirror, observer) {
+        for (var j = 0, jlen = mirror.observers.length; j < jlen; j++) {
+            if (mirror.observers[j].observer === observer) {
+                mirror.observers.splice(j, 1);
+                return;
+            }
+        }
+    }
+    function unobserve(root, observer) {
+        observer.unobserve();
+    }
+    jsonpatch.unobserve = unobserve;
+    function deepClone(obj) {
+        if (typeof obj === "object") {
+            return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5
+        }
+        else {
+            return obj; //no need to clone primitives
+        }
+    }
+    function observe(obj, callback) {
+        var patches = [];
+        var root = obj;
+        var observer;
+        var mirror = getMirror(obj);
+        if (!mirror) {
+            mirror = new Mirror(obj);
+            beforeDict.push(mirror);
+        }
+        else {
+            observer = getObserverFromMirror(mirror, callback);
+        }
+        if (observer) {
+            return observer;
+        }
+        observer = {};
+        mirror.value = deepClone(obj);
+        if (callback) {
+            observer.callback = callback;
+            observer.next = null;
+            var intervals = this.intervals || [100, 1000, 10000, 60000];
+            if (intervals.push === void 0) {
+                throw new OriginalError("jsonpatch.intervals must be an array");
+            }
+            var currentInterval = 0;
+            var dirtyCheck = function () {
+                generate(observer);
+            };
+            var fastCheck = function () {
+                clearTimeout(observer.next);
+                observer.next = setTimeout(function () {
+                    dirtyCheck();
+                    currentInterval = 0;
+                    observer.next = setTimeout(slowCheck, intervals[currentInterval++]);
+                }, 0);
+            };
+            var slowCheck = function () {
+                dirtyCheck();
+                if (currentInterval == intervals.length)
+                    currentInterval = intervals.length - 1;
+                observer.next = setTimeout(slowCheck, intervals[currentInterval++]);
+            };
+            if (typeof window !== 'undefined') {
+                if (window.addEventListener) {
+                    window.addEventListener('mousedown', fastCheck);
+                    window.addEventListener('mouseup', fastCheck);
+                    window.addEventListener('keydown', fastCheck);
+                }
+                else {
+                    document.documentElement.attachEvent('onmousedown', fastCheck);
+                    document.documentElement.attachEvent('onmouseup', fastCheck);
+                    document.documentElement.attachEvent('onkeydown', fastCheck);
+                }
+            }
+            observer.next = setTimeout(slowCheck, intervals[currentInterval++]);
+        }
+        observer.patches = patches;
+        observer.object = obj;
+        observer.unobserve = function () {
+            generate(observer);
+            clearTimeout(observer.next);
+            removeObserverFromMirror(mirror, observer);
+            if (mirror.observers.length === 0) {
+                beforeDict.splice(beforeDict.indexOf(mirror), 1);
+            }
+            if (typeof window !== 'undefined') {
+                if (window.removeEventListener) {
+                    window.removeEventListener('mousedown', fastCheck);
+                    window.removeEventListener('mouseup', fastCheck);
+                    window.removeEventListener('keydown', fastCheck);
+                }
+                else {
+                    document.documentElement.detachEvent('onmousedown', fastCheck);
+                    document.documentElement.detachEvent('onmouseup', fastCheck);
+                    document.documentElement.detachEvent('onkeydown', fastCheck);
+                }
+            }
+        };
+        mirror.observers.push(new ObserverInfo(callback, observer));
+        return observer;
+    }
+    jsonpatch.observe = observe;
+    function generate(observer) {
+        var mirror;
+        for (var i = 0, ilen = beforeDict.length; i < ilen; i++) {
+            if (beforeDict[i].obj === observer.object) {
+                mirror = beforeDict[i];
+                break;
+            }
+        }
+        _generate(mirror.value, observer.object, observer.patches, "");
+        if (observer.patches.length) {
+            apply(mirror.value, observer.patches);
+        }
+        var temp = observer.patches;
+        if (temp.length > 0) {
+            observer.patches = [];
+            if (observer.callback) {
+                observer.callback(temp);
+            }
+        }
+        return temp;
+    }
+    jsonpatch.generate = generate;
+    // Dirty check if obj is different from mirror, generate patches and update mirror
+    function _generate(mirror, obj, patches, path) {
+        var newKeys = _objectKeys(obj);
+        var oldKeys = _objectKeys(mirror);
+        var changed = false;
+        var deleted = false;
+        //if ever "move" operation is implemented here, make sure this test runs OK: "should not generate the same patch twice (move)"
+        for (var t = oldKeys.length - 1; t >= 0; t--) {
+            var key = oldKeys[t];
+            var oldVal = mirror[key];
+            if (obj.hasOwnProperty(key)) {
+                var newVal = obj[key];
+                if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null) {
+                    _generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key));
+                }
+                else {
+                    if (oldVal != newVal) {
+                        changed = true;
+                        patches.push({ op: "replace", path: path + "/" + escapePathComponent(key), value: deepClone(newVal) });
+                    }
+                }
+            }
+            else {
+                patches.push({ op: "remove", path: path + "/" + escapePathComponent(key) });
+                deleted = true; // property has been deleted
+            }
+        }
+        if (!deleted && newKeys.length == oldKeys.length) {
+            return;
+        }
+        for (var t = 0; t < newKeys.length; t++) {
+            var key = newKeys[t];
+            if (!mirror.hasOwnProperty(key)) {
+                patches.push({ op: "add", path: path + "/" + escapePathComponent(key), value: deepClone(obj[key]) });
+            }
+        }
+    }
+    var _isArray;
+    if (Array.isArray) {
+        _isArray = Array.isArray;
+    }
+    else {
+        _isArray = function (obj) {
+            return obj.push && typeof obj.length === 'number';
+        };
+    }
+    //3x faster than cached /^\d+$/.test(str)
+    function isInteger(str) {
+        var i = 0;
+        var len = str.length;
+        var charCode;
+        while (i < len) {
+            charCode = str.charCodeAt(i);
+            if (charCode >= 48 && charCode <= 57) {
+                i++;
+                continue;
+            }
+            return false;
+        }
+        return true;
+    }
+    /// Apply a json-patch operation on an object tree
+    function apply(tree, patches, validate) {
+        var result = false, p = 0, plen = patches.length, patch, key;
+        while (p < plen) {
+            patch = patches[p];
+            p++;
+            // Find the object
+            var path = patch.path || "";
+            var keys = path.split('/');
+            var obj = tree;
+            var t = 1; //skip empty element - http://jsperf.com/to-shift-or-not-to-shift
+            var len = keys.length;
+            var existingPathFragment = undefined;
+            while (true) {
+                key = keys[t];
+                if (validate) {
+                    if (existingPathFragment === undefined) {
+                        if (obj[key] === undefined) {
+                            existingPathFragment = keys.slice(0, t).join('/');
+                        }
+                        else if (t == len - 1) {
+                            existingPathFragment = patch.path;
+                        }
+                        if (existingPathFragment !== undefined) {
+                            this.validator(patch, p - 1, tree, existingPathFragment);
+                        }
+                    }
+                }
+                t++;
+                if (key === undefined) {
+                    if (t >= len) {
+                        result = rootOps[patch.op].call(patch, obj, key, tree); // Apply patch
+                        break;
+                    }
+                }
+                if (_isArray(obj)) {
+                    if (key === '-') {
+                        key = obj.length;
+                    }
+                    else {
+                        if (validate && !isInteger(key)) {
+                            throw new JsonPatchError("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index", "OPERATION_PATH_ILLEGAL_ARRAY_INDEX", p - 1, patch.path, patch);
+                        }
+                        key = parseInt(key, 10);
+                    }
+                    if (t >= len) {
+                        if (validate && patch.op === "add" && key > obj.length) {
+                            throw new JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array", "OPERATION_VALUE_OUT_OF_BOUNDS", p - 1, patch.path, patch);
+                        }
+                        result = arrOps[patch.op].call(patch, obj, key, tree); // Apply patch
+                        break;
+                    }
+                }
+                else {
+                    if (key && key.indexOf('~') != -1)
+                        key = key.replace(/~1/g, '/').replace(/~0/g, '~'); // escape chars
+                    if (t >= len) {
+                        result = objOps[patch.op].call(patch, obj, key, tree); // Apply patch
+                        break;
+                    }
+                }
+                obj = obj[key];
+            }
+        }
+        return result;
+    }
+    jsonpatch.apply = apply;
+    function compare(tree1, tree2) {
+        var patches = [];
+        _generate(tree1, tree2, patches, '');
+        return patches;
+    }
+    jsonpatch.compare = compare;
+    var JsonPatchError = (function (_super) {
+        __extends(JsonPatchError, _super);
+        function JsonPatchError(message, name, index, operation, tree) {
+            _super.call(this, message);
+            this.message = message;
+            this.name = name;
+            this.index = index;
+            this.operation = operation;
+            this.tree = tree;
+        }
+        return JsonPatchError;
+    })(OriginalError);
+    jsonpatch.JsonPatchError = JsonPatchError;
+    jsonpatch.Error = JsonPatchError;
+    /**
+     * Recursively checks whether an object has any undefined values inside.
+     */
+    function hasUndefined(obj) {
+        if (obj === undefined) {
+            return true;
+        }
+        if (typeof obj == "array" || typeof obj == "object") {
+            for (var i in obj) {
+                if (hasUndefined(obj[i])) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+    /**
+     * Validates a single operation. Called from `jsonpatch.validate`. Throws `JsonPatchError` in case of an error.
+     * @param {object} operation - operation object (patch)
+     * @param {number} index - index of operation in the sequence
+     * @param {object} [tree] - object where the operation is supposed to be applied
+     * @param {string} [existingPathFragment] - comes along with `tree`
+     */
+    function validator(operation, index, tree, existingPathFragment) {
+        if (typeof operation !== 'object' || operation === null || _isArray(operation)) {
+            throw new JsonPatchError('Operation is not an object', 'OPERATION_NOT_AN_OBJECT', index, operation, tree);
+        }
+        else if (!objOps[operation.op]) {
+            throw new JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, tree);
+        }
+        else if (typeof operation.path !== 'string') {
+            throw new JsonPatchError('Operation `path` property is not a string', 'OPERATION_PATH_INVALID', index, operation, tree);
+        }
+        else if ((operation.op === 'move' || operation.op === 'copy') && typeof operation.from !== 'string') {
+            throw new JsonPatchError('Operation `from` property is not present (applicable in `move` and `copy` operations)', 'OPERATION_FROM_REQUIRED', index, operation, tree);
+        }
+        else if ((operation.op === 'add' || operation.op === 'replace' || operation.op === 'test') && operation.value === undefined) {
+            throw new JsonPatchError('Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', 'OPERATION_VALUE_REQUIRED', index, operation, tree);
+        }
+        else if ((operation.op === 'add' || operation.op === 'replace' || operation.op === 'test') && hasUndefined(operation.value)) {
+            throw new JsonPatchError('Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', 'OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED', index, operation, tree);
+        }
+        else if (tree) {
+            if (operation.op == "add") {
+                var pathLen = operation.path.split("/").length;
+                var existingPathLen = existingPathFragment.split("/").length;
+                if (pathLen !== existingPathLen + 1 && pathLen !== existingPathLen) {
+                    throw new JsonPatchError('Cannot perform an `add` operation at the desired path', 'OPERATION_PATH_CANNOT_ADD', index, operation, tree);
+                }
+            }
+            else if (operation.op === 'replace' || operation.op === 'remove' || operation.op === '_get') {
+                if (operation.path !== existingPathFragment) {
+                    throw new JsonPatchError('Cannot perform the operation at a path that does not exist', 'OPERATION_PATH_UNRESOLVABLE', index, operation, tree);
+                }
+            }
+            else if (operation.op === 'move' || operation.op === 'copy') {
+                var existingValue = { op: "_get", path: operation.from, value: undefined };
+                var error = jsonpatch.validate([existingValue], tree);
+                if (error && error.name === 'OPERATION_PATH_UNRESOLVABLE') {
+                    throw new JsonPatchError('Cannot perform the operation from a path that does not exist', 'OPERATION_FROM_UNRESOLVABLE', index, operation, tree);
+                }
+            }
+        }
+    }
+    jsonpatch.validator = validator;
+    /**
+     * Validates a sequence of operations. If `tree` parameter is provided, the sequence is additionally validated against the object tree.
+     * If error is encountered, returns a JsonPatchError object
+     * @param sequence
+     * @param tree
+     * @returns {JsonPatchError|undefined}
+     */
+    function validate(sequence, tree) {
+        try {
+            if (!_isArray(sequence)) {
+                throw new JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY');
+            }
+            if (tree) {
+                tree = JSON.parse(JSON.stringify(tree)); //clone tree so that we can safely try applying operations
+                apply.call(this, tree, sequence, true);
+            }
+            else {
+                for (var i = 0; i < sequence.length; i++) {
+                    this.validator(sequence[i], i);
+                }
+            }
+        }
+        catch (e) {
+            if (e instanceof JsonPatchError) {
+                return e;
+            }
+            else {
+                throw e;
+            }
+        }
+    }
+    jsonpatch.validate = validate;
+})(jsonpatch || (jsonpatch = {}));
+if (typeof exports !== "undefined") {
+    exports.apply = jsonpatch.apply;
+    exports.observe = jsonpatch.observe;
+    exports.unobserve = jsonpatch.unobserve;
+    exports.generate = jsonpatch.generate;
+    exports.compare = jsonpatch.compare;
+    exports.validate = jsonpatch.validate;
+    exports.validator = jsonpatch.validator;
+    exports.JsonPatchError = jsonpatch.JsonPatchError;
+    exports.Error = jsonpatch.Error;
+}

+ 138 - 0
handsontable/package.json

@@ -0,0 +1,138 @@
+{
+  "name": "handsontable",
+  "description": "Handsontable is a JavaScript Spreadsheet Component available for React, Angular and Vue.",
+  "homepage": "https://handsontable.com/",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/handsontable/handsontable.git"
+  },
+  "bugs": {
+    "url": "https://github.com/handsontable/handsontable/issues"
+  },
+  "author": "Handsoncode <hello@handsontable.com>",
+  "version": "6.2.2",
+  "browser": "dist/handsontable.js",
+  "main": "commonjs/index.js",
+  "module": "es/index.js",
+  "jsnext:main": "es/index.js",
+  "scripts": {
+    "clean": "rimraf commonjs es coverage",
+    "lint": "eslint src/ test/",
+    "test": "npm run lint && npm run test:unit && npm run test:types && npm run test:walkontable && npm run test:e2e && npm run test:production",
+    "test:walkontable": "npm run build:walkontable && npm run test:walkontable.dump && npm run test:walkontable.puppeteer",
+    "test:production": "npm run build:umd.min && npm run build:languages.min && npm run test:production.dump && npm run test:e2e.puppeteer",
+    "test:e2e": "npm run build:umd && npm run build:languages && npm run test:e2e.dump && npm run test:e2e.puppeteer",
+    "test:e2e.watch": "node ./.config/bin/trigger-on-stdout-change.js \"concurrently --raw --kill-others 'npm run build:umd -- --watch' 'npm run test:e2e.dump -- --watch'\" \"npm run test:e2e.puppeteer\"",
+    "test:e2e.puppeteer": "node test/scripts/run-puppeteer.js test/E2ERunner.html",
+    "test:unit": "cross-env npm_config_testPathPattern=. BABEL_ENV=commonjs env-cmd ./hot.config.js jest --testPathPattern=$npm_config_testPathPattern",
+    "test:unit.watch": "cross-env BABEL_ENV=commonjs env-cmd ./hot.config.js jest --testPathPattern=$npm_config_testPathPattern --watch",
+    "test:walkontable.puppeteer": "node test/scripts/run-puppeteer.js src/3rdparty/walkontable/test/SpecRunner.html",
+    "test:walkontable.dump": "cross-env BABEL_ENV=commonjs_e2e NODE_ENV=test-walkontable env-cmd ./hot.config.js webpack --hide-modules helpers=./src/3rdparty/walkontable/test/helpers/index.js specs=./src/3rdparty/walkontable/test/spec/index.js",
+    "test:production.dump": "cross-env BABEL_ENV=commonjs_e2e NODE_ENV=test-production env-cmd ./hot.config.js webpack --hide-modules helpers=./test/helpers/index.js e2e=./test/e2e/index.js",
+    "test:e2e.dump": "cross-env BABEL_ENV=commonjs_e2e NODE_ENV=test-e2e env-cmd ./hot.config.js webpack --hide-modules helpers=./test/helpers/index.js e2e=./test/e2e/index.js",
+    "test:mobile.dump": "cross-env BABEL_ENV=commonjs_e2e NODE_ENV=test-mobile env-cmd ./hot.config.js webpack --hide-modules helpers=./test/helpers/index.js mobile=./test/e2e/mobile/index.js",
+    "test:types": "tsc -p ./test/types",
+    "build": "npm run build:commonjs && npm run build:es && npm run build:umd && npm run build:umd.min && npm run build:languages && npm run build:languages.min",
+    "build:commonjs": "cross-env BABEL_ENV=commonjs_dist env-cmd ./hot.config.js babel src --out-dir commonjs",
+    "build:es": "cross-env BABEL_ENV=es env-cmd ./hot.config.js babel src --out-dir es",
+    "build:umd": "cross-env BABEL_ENV=commonjs NODE_ENV=development env-cmd ./hot.config.js webpack --hide-modules ./src/index.js",
+    "build:umd.min": "cross-env BABEL_ENV=commonjs NODE_ENV=production env-cmd ./hot.config.js webpack --hide-modules ./src/index.js",
+    "build:walkontable": "cross-env BABEL_ENV=commonjs NODE_ENV=walkontable env-cmd ./hot.config.js webpack --hide-modules ./src/3rdparty/walkontable/src/index.js",
+    "build:languages": "cross-env BABEL_ENV=commonjs NODE_ENV=languages-development env-cmd ./hot.config.js webpack",
+    "build:languages.min": "cross-env BABEL_ENV=commonjs NODE_ENV=languages-production env-cmd ./hot.config.js webpack",
+    "watch": "cross-env BABEL_ENV=commonjs NODE_ENV=watch env-cmd ./hot.config.js webpack --hide-modules --watch src/index.js",
+    "prepublish": "npm run clean && npm run build:commonjs && npm run build:es"
+  },
+  "keywords": [
+    "data",
+    "grid",
+    "table",
+    "editor",
+    "grid-editor",
+    "data-grid",
+    "data-table",
+    "spreadsheet",
+    "excel",
+    "tabular-data",
+    "edit-cell",
+    "editable-table",
+    "data-spreadsheet"
+  ],
+  "dependencies": {
+    "moment": "2.20.1",
+    "numbro": "^2.0.6",
+    "pikaday": "1.5.1"
+  },
+  "devDependencies": {
+    "@babel/cli": "^7.1.5",
+    "@babel/core": "^7.1.6",
+    "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
+    "@babel/polyfill": "^7.0.0",
+    "@babel/preset-env": "^7.1.6",
+    "@babel/register": "^7.0.0",
+    "@typescript-eslint/eslint-plugin": "^4.12.0",
+    "@typescript-eslint/parser": "^4.12.0",
+    "babel-core": "7.0.0-bridge.0",
+    "babel-eslint": "^7.2.3",
+    "babel-jest": "^23.6.0",
+    "babel-loader": "^8.0.4",
+    "babel-plugin-forbidden-imports": "^0.1.2",
+    "babel-plugin-transform-inline-environment-variables": "^0.4.3",
+    "babel-plugin-transform-require-ignore": "^0.1.1",
+    "check-es3-syntax-cli": "^0.2.0",
+    "concurrently": "^3.5.0",
+    "copy-webpack-plugin": "^4.6.0",
+    "cross-env": "^5.0.1",
+    "css-loader": "^0.28.4",
+    "ecstatic": "^3.2.0",
+    "env-cmd": "^5.1.0",
+    "eslint": "^7.17.0",
+    "eslint-config-airbnb-base": "^11.3.1",
+    "eslint-plugin-import": "^2.7.0",
+    "extract-text-webpack-plugin": "^3.0.0",
+    "fs-extra": "^4.0.2",
+    "glob": "^7.1.2",
+    "html-webpack-plugin": "^2.30.1",
+    "jasmine-co": "^1.2.2",
+    "jasmine-core": "2.5.2",
+    "jasmine-terminal-reporter": "^1.0.3",
+    "jest": "^23.6.0",
+    "jest-cli": "^23.6.0",
+    "loader-utils": "^1.1.0",
+    "on-build-webpack": "^0.1.0",
+    "optimize-css-assets-webpack-plugin": "^3.1.1",
+    "progress-bar-webpack-plugin": "^1.10.0",
+    "puppeteer": "^1.10.0",
+    "request": "^2.81.0",
+    "rimraf": "^2.5.4",
+    "spawn-command": "0.0.2",
+    "string-replace-webpack-plugin": "^0.1.3",
+    "style-loader": "^0.18.2",
+    "supports-color": "^4.2.1",
+    "tree-kill": "^1.2.1",
+    "typescript": "^2.4.2",
+    "uglifyjs-webpack-plugin": "^1.3.0",
+    "webpack": "^3.5.6"
+  },
+  "license": "MIT",
+  "resolutions": {
+    "babel-core": "7.0.0-bridge.0"
+  },
+  "jest": {
+    "roots": [
+      "./test/unit",
+      "./src/plugins"
+    ],
+    "setupTestFrameworkScriptFile": "<rootDir>/test/bootstrap.js",
+    "testRegex": "(\\.spec\\.js)|(\\.unit\\.js)$",
+    "testPathIgnorePatterns": [
+      "<rootDir>/node_modules/"
+    ],
+    "moduleNameMapper": {
+      "^handsontable(.*)$": "<rootDir>/src$1",
+      "^walkontable(.*)$": "<rootDir>/src/3rdparty/walkontable/src$1",
+      "\\.css$": "<rootDir>/test/__mocks__/styleMock.js"
+    }
+  },
+  "typings": "./handsontable.d.ts"
+}

+ 148 - 0
handsontable/src/3rdparty/walkontable/css/bootstrap.css

@@ -0,0 +1,148 @@
+/*!
+ * Below is an unaltered fragment from Twitter Bootstrap
+ */
+
+/*!
+ * Bootstrap v2.2.1
+ *
+ * Copyright 2012 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world @twitter by @mdo and @fat.
+ */
+
+table {
+  max-width: 100%;
+  background-color: transparent;
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+.table {
+  width: 100%;
+  margin-bottom: 20px;
+}
+
+.table th,
+.table td {
+  padding: 8px;
+  line-height: 20px;
+  text-align: left;
+  vertical-align: top;
+  border-top: 1px solid #dddddd;
+}
+
+.table th {
+  font-weight: bold;
+}
+
+.table thead th {
+  vertical-align: bottom;
+}
+
+.table caption + thead tr:first-child th,
+.table caption + thead tr:first-child td,
+.table colgroup + thead tr:first-child th,
+.table colgroup + thead tr:first-child td,
+.table thead:first-child tr:first-child th,
+.table thead:first-child tr:first-child td {
+  border-top: 0;
+}
+
+.table tbody + tbody {
+  border-top: 2px solid #dddddd;
+}
+
+.table-condensed th,
+.table-condensed td {
+  padding: 4px 5px;
+}
+
+.table-bordered {
+  border: 1px solid #dddddd;
+  border-collapse: separate;
+  *border-collapse: collapse;
+  border-left: 0;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+
+.table-bordered th,
+.table-bordered td {
+  border-left: 1px solid #dddddd;
+}
+
+.table-bordered caption + thead tr:first-child th,
+.table-bordered caption + tbody tr:first-child th,
+.table-bordered caption + tbody tr:first-child td,
+.table-bordered colgroup + thead tr:first-child th,
+.table-bordered colgroup + tbody tr:first-child th,
+.table-bordered colgroup + tbody tr:first-child td,
+.table-bordered thead:first-child tr:first-child th,
+.table-bordered tbody:first-child tr:first-child th,
+.table-bordered tbody:first-child tr:first-child td {
+  border-top: 0;
+}
+
+.table-bordered thead:first-child tr:first-child th:first-child,
+.table-bordered tbody:first-child tr:first-child td:first-child {
+  -webkit-border-top-left-radius: 4px;
+  border-top-left-radius: 4px;
+  -moz-border-radius-topleft: 4px;
+}
+
+.table-bordered thead:first-child tr:first-child th:last-child,
+.table-bordered tbody:first-child tr:first-child td:last-child {
+  -webkit-border-top-right-radius: 4px;
+  border-top-right-radius: 4px;
+  -moz-border-radius-topright: 4px;
+}
+
+.table-bordered thead:last-child tr:last-child th:first-child,
+.table-bordered tbody:last-child tr:last-child td:first-child,
+.table-bordered tfoot:last-child tr:last-child td:first-child {
+  -webkit-border-radius: 0 0 0 4px;
+  -moz-border-radius: 0 0 0 4px;
+  border-radius: 0 0 0 4px;
+  -webkit-border-bottom-left-radius: 4px;
+  border-bottom-left-radius: 4px;
+  -moz-border-radius-bottomleft: 4px;
+}
+
+.table-bordered thead:last-child tr:last-child th:last-child,
+.table-bordered tbody:last-child tr:last-child td:last-child,
+.table-bordered tfoot:last-child tr:last-child td:last-child {
+  -webkit-border-bottom-right-radius: 4px;
+  border-bottom-right-radius: 4px;
+  -moz-border-radius-bottomright: 4px;
+}
+
+.table-bordered caption + thead tr:first-child th:first-child,
+.table-bordered caption + tbody tr:first-child td:first-child,
+.table-bordered colgroup + thead tr:first-child th:first-child,
+.table-bordered colgroup + tbody tr:first-child td:first-child {
+  -webkit-border-top-left-radius: 4px;
+  border-top-left-radius: 4px;
+  -moz-border-radius-topleft: 4px;
+}
+
+.table-bordered caption + thead tr:first-child th:last-child,
+.table-bordered caption + tbody tr:first-child td:last-child,
+.table-bordered colgroup + thead tr:first-child th:last-child,
+.table-bordered colgroup + tbody tr:first-child td:last-child {
+  -webkit-border-top-right-radius: 4px;
+  border-top-right-radius: 4px;
+  -moz-border-radius-topright: 4px;
+}
+
+.table-striped tbody tr:nth-child(odd) td,
+.table-striped tbody tr:nth-child(odd) th {
+  background-color: #f9f9f9;
+}
+
+.table-hover tbody tr:hover td,
+.table-hover tbody tr:hover th {
+  background-color: #f5f5f5;
+}

+ 164 - 0
handsontable/src/3rdparty/walkontable/css/walkontable.css

@@ -0,0 +1,164 @@
+.wtHolder {
+  position: relative;
+  font-family: Arial, Helvetica, sans-serif;
+  line-height: 1.3em;
+  font-size: 13px;
+}
+
+.wtHolder table,
+.wtHolder tbody,
+.wtHolder thead,
+.wtHolder td,
+.wtHolder th,
+.wtHolder div {
+  box-sizing: content-box;
+  -webkit-box-sizing: content-box;
+  -moz-box-sizing: content-box;
+}
+
+.wtHolder table {
+  border-collapse: separate;
+  /*it must be separate, otherwise there are offset miscalculations in WebKit: http://stackoverflow.com/questions/2655987/border-collapse-differences-in-ff-and-webkit*/
+  /*position: relative;*/
+  /*this actually only changes appearance of user selection - does not make text unselectable
+  -webkit-user-select: none;
+  -khtml-user-select: none;
+  -moz-user-select: none;
+  -o-user-select: none;
+  -ms-user-select: none;
+  /*user-select: none; /*no browser supports unprefixed version*/
+  border-spacing: 0;
+  margin: 0;
+  border-width: 0;
+  table-layout: fixed;
+  width: 0;
+  outline-width: 0;
+  /* reset bootstrap table style. for more info see: https://github.com/handsontable/handsontable/issues/224 */
+  max-width: none;
+  max-height: none;
+}
+
+.wtHolder col {
+  width: 50px;
+}
+
+.wtHolder col.rowHeader {
+  width: 50px;
+}
+
+.wtHolder th,
+.wtHolder td {
+  border-right: 1px solid #CCC;
+  border-bottom: 1px solid #CCC;
+  height: 22px;
+  empty-cells: show;
+  line-height: 21px;
+  padding: 0 4px 0 4px;
+  /* top, bottom padding different than 0 is handled poorly by FF with HTML5 doctype */
+  background-color: #FFF;
+  vertical-align: top;
+  overflow: hidden;
+  outline-width: 0;
+  white-space: pre-line;
+  /* preserve new line character in cell */
+}
+
+.wtHolder th:last-child {
+  /*Foundation framework fix*/
+  border-right: 1px solid #CCC;
+  border-bottom: 1px solid #CCC;
+}
+
+.wtHolder tr:first-child th.htNoFrame,
+.wtHolder th:first-child.htNoFrame,
+.wtHolder th.htNoFrame {
+  border-left-width: 0;
+  background-color: white;
+  border-color: #FFF;
+}
+
+.wtHolder th:first-child,
+.wtHolder td:first-child,
+.wtHolder .htNoFrame + th,
+.wtHolder .htNoFrame + td {
+  border-left: 1px solid #CCC;
+}
+
+.wtHolder tr:first-child th,
+.wtHolder tr:first-child td {
+  border-top: 1px solid #CCC;
+}
+
+.wtHolder thead tr:last-child th {
+  border-bottom-width: 0;
+}
+
+.wtHolder thead tr.lastChild th {
+  border-bottom-width: 0;
+}
+
+.wtHolder th {
+  background-color: #EEE;
+  color: #222;
+  text-align: center;
+  font-weight: normal;
+  white-space: nowrap;
+}
+
+.wtHolder th .small {
+  font-size: 12px;
+}
+
+.wtHolder thead th {
+  padding: 0;
+}
+
+.wtHolder th.active {
+  background-color: #CCC;
+}
+
+.wtHolder thead th .relative {
+  position: relative;
+  padding: 2px 4px;
+}
+
+/* border line */
+.wtHolder .wtBorder {
+  position: absolute;
+  font-size: 0;
+}
+
+.wtHolder td.area {
+  background-color: #EEF4FF;
+}
+
+/* fill handle */
+.wtHolder .wtBorder.corner {
+  font-size: 0;
+  cursor: crosshair;
+}
+
+.wtHolder .htBorder.htFillBorder {
+  background: red;
+  width: 1px;
+  height: 1px;
+}
+
+.ht_master .wtHolder {
+  overflow: auto;
+}
+
+.ht_clone_left .wtHolder {
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.ht_clone_top .wtHolder {
+  overflow-x: auto;
+  overflow-y: hidden;
+}
+
+/* used for testing */
+.testOverflowHidden {
+  overflow: hidden;
+}

+ 15 - 0
handsontable/src/3rdparty/walkontable/package.json

@@ -0,0 +1,15 @@
+{
+  "name": "walkontable",
+  "description": "Table renderer for Handsontable",
+  "homepage": "http://handsontable.com/",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/handsontable/handsontable.git"
+  },
+  "bugs": {
+    "url": "https://github.com/handsontable/handsontable/issues"
+  },
+  "author": "Handsoncode <hello@handsontable.com>",
+  "version": "0.0.1",
+  "license": "MIT"
+}

+ 674 - 0
handsontable/src/3rdparty/walkontable/src/border.js

@@ -0,0 +1,674 @@
+import {
+  addClass,
+  hasClass,
+  removeClass,
+  getComputedStyle,
+  getTrimmingContainer,
+  innerWidth,
+  innerHeight,
+  offset,
+  outerHeight,
+  outerWidth,
+} from './../../../helpers/dom/element';
+import { stopImmediatePropagation } from './../../../helpers/dom/event';
+import { objectEach } from './../../../helpers/object';
+import { isMobileBrowser } from './../../../helpers/browser';
+import EventManager from './../../../eventManager';
+import CellCoords from './cell/coords';
+
+/**
+ *
+ */
+class Border {
+  /**
+   * @param {Walkontable} wotInstance
+   * @param {Object} settings
+   */
+  constructor(wotInstance, settings) {
+    if (!settings) {
+      return;
+    }
+    this.eventManager = new EventManager(wotInstance);
+    this.instance = wotInstance;
+    this.wot = wotInstance;
+    this.settings = settings;
+    this.mouseDown = false;
+    this.main = null;
+
+    this.top = null;
+    this.left = null;
+    this.bottom = null;
+    this.right = null;
+
+    this.topStyle = null;
+    this.leftStyle = null;
+    this.bottomStyle = null;
+    this.rightStyle = null;
+
+    this.cornerDefaultStyle = {
+      width: '6px',
+      height: '6px',
+      borderWidth: '1px',
+      borderStyle: 'solid',
+      borderColor: '#FFF'
+    };
+    this.corner = null;
+    this.cornerStyle = null;
+
+    this.createBorders(settings);
+    this.registerListeners();
+  }
+
+  /**
+   * Register all necessary events
+   */
+  registerListeners() {
+    this.eventManager.addEventListener(document.body, 'mousedown', () => this.onMouseDown());
+    this.eventManager.addEventListener(document.body, 'mouseup', () => this.onMouseUp());
+
+    for (let c = 0, len = this.main.childNodes.length; c < len; c++) {
+      this.eventManager.addEventListener(this.main.childNodes[c], 'mouseenter', event => this.onMouseEnter(event, this.main.childNodes[c]));
+    }
+  }
+
+  /**
+   * Mouse down listener
+   *
+   * @private
+   */
+  onMouseDown() {
+    this.mouseDown = true;
+  }
+
+  /**
+   * Mouse up listener
+   *
+   * @private
+   */
+  onMouseUp() {
+    this.mouseDown = false;
+  }
+
+  /**
+   * Mouse enter listener for fragment selection functionality.
+   *
+   * @private
+   * @param {Event} event Dom event
+   * @param {HTMLElement} parentElement Part of border element.
+   */
+  onMouseEnter(event, parentElement) {
+    if (!this.mouseDown || !this.wot.getSetting('hideBorderOnMouseDownOver')) {
+      return;
+    }
+    event.preventDefault();
+    stopImmediatePropagation(event);
+
+    const _this = this;
+    const bounds = parentElement.getBoundingClientRect();
+    // Hide border to prevents selection jumping when fragmentSelection is enabled.
+    parentElement.style.display = 'none';
+
+    function isOutside(mouseEvent) {
+      if (mouseEvent.clientY < Math.floor(bounds.top)) {
+        return true;
+      }
+      if (mouseEvent.clientY > Math.ceil(bounds.top + bounds.height)) {
+        return true;
+      }
+      if (mouseEvent.clientX < Math.floor(bounds.left)) {
+        return true;
+      }
+      if (mouseEvent.clientX > Math.ceil(bounds.left + bounds.width)) {
+        return true;
+      }
+    }
+
+    function handler(handlerEvent) {
+      if (isOutside(handlerEvent)) {
+        _this.eventManager.removeEventListener(document.body, 'mousemove', handler);
+        parentElement.style.display = 'block';
+      }
+    }
+
+    this.eventManager.addEventListener(document.body, 'mousemove', handler);
+  }
+
+  /**
+   * Create border elements
+   *
+   * @param {Object} settings
+   */
+  createBorders(settings) {
+    this.main = document.createElement('div');
+
+    const borderDivs = ['top', 'left', 'bottom', 'right', 'corner'];
+    let style = this.main.style;
+    style.position = 'absolute';
+    style.top = 0;
+    style.left = 0;
+
+    for (let i = 0; i < 5; i++) {
+      const position = borderDivs[i];
+      const div = document.createElement('div');
+
+      div.className = `wtBorder ${this.settings.className || ''}`; // + borderDivs[i];
+
+      if (this.settings[position] && this.settings[position].hide) {
+        div.className += ' hidden';
+      }
+      style = div.style;
+      style.backgroundColor = (this.settings[position] && this.settings[position].color) ? this.settings[position].color : settings.border.color;
+      style.height = (this.settings[position] && this.settings[position].width) ? `${this.settings[position].width}px` : `${settings.border.width}px`;
+      style.width = (this.settings[position] && this.settings[position].width) ? `${this.settings[position].width}px` : `${settings.border.width}px`;
+
+      this.main.appendChild(div);
+    }
+    this.top = this.main.childNodes[0];
+    this.left = this.main.childNodes[1];
+    this.bottom = this.main.childNodes[2];
+    this.right = this.main.childNodes[3];
+
+    this.topStyle = this.top.style;
+    this.leftStyle = this.left.style;
+    this.bottomStyle = this.bottom.style;
+    this.rightStyle = this.right.style;
+
+    this.corner = this.main.childNodes[4];
+    this.corner.className += ' corner';
+    this.cornerStyle = this.corner.style;
+    this.cornerStyle.width = this.cornerDefaultStyle.width;
+    this.cornerStyle.height = this.cornerDefaultStyle.height;
+    this.cornerStyle.border = [
+      this.cornerDefaultStyle.borderWidth,
+      this.cornerDefaultStyle.borderStyle,
+      this.cornerDefaultStyle.borderColor
+    ].join(' ');
+
+    if (isMobileBrowser()) {
+      this.createMultipleSelectorHandles();
+    }
+    this.disappear();
+
+    let bordersHolder = this.wot.wtTable.bordersHolder;
+
+    if (!bordersHolder) {
+      bordersHolder = document.createElement('div');
+      bordersHolder.className = 'htBorders';
+      this.wot.wtTable.bordersHolder = bordersHolder;
+      this.wot.wtTable.spreader.appendChild(bordersHolder);
+    }
+    bordersHolder.appendChild(this.main);
+  }
+
+  /**
+   * Create multiple selector handler for mobile devices
+   */
+  createMultipleSelectorHandles() {
+    this.selectionHandles = {
+      topLeft: document.createElement('DIV'),
+      topLeftHitArea: document.createElement('DIV'),
+      bottomRight: document.createElement('DIV'),
+      bottomRightHitArea: document.createElement('DIV')
+    };
+    const width = 10;
+    const hitAreaWidth = 40;
+
+    this.selectionHandles.topLeft.className = 'topLeftSelectionHandle';
+    this.selectionHandles.topLeftHitArea.className = 'topLeftSelectionHandle-HitArea';
+    this.selectionHandles.bottomRight.className = 'bottomRightSelectionHandle';
+    this.selectionHandles.bottomRightHitArea.className = 'bottomRightSelectionHandle-HitArea';
+
+    this.selectionHandles.styles = {
+      topLeft: this.selectionHandles.topLeft.style,
+      topLeftHitArea: this.selectionHandles.topLeftHitArea.style,
+      bottomRight: this.selectionHandles.bottomRight.style,
+      bottomRightHitArea: this.selectionHandles.bottomRightHitArea.style
+    };
+
+    const hitAreaStyle = {
+      position: 'absolute',
+      height: `${hitAreaWidth}px`,
+      width: `${hitAreaWidth}px`,
+      'border-radius': `${parseInt(hitAreaWidth / 1.5, 10)}px`,
+    };
+
+    objectEach(hitAreaStyle, (value, key) => {
+      this.selectionHandles.styles.bottomRightHitArea[key] = value;
+      this.selectionHandles.styles.topLeftHitArea[key] = value;
+    });
+
+    const handleStyle = {
+      position: 'absolute',
+      height: `${width}px`,
+      width: `${width}px`,
+      'border-radius': `${parseInt(width / 1.5, 10)}px`,
+      background: '#F5F5FF',
+      border: '1px solid #4285c8'
+    };
+
+    objectEach(handleStyle, (value, key) => {
+      this.selectionHandles.styles.bottomRight[key] = value;
+      this.selectionHandles.styles.topLeft[key] = value;
+    });
+
+    this.main.appendChild(this.selectionHandles.topLeft);
+    this.main.appendChild(this.selectionHandles.bottomRight);
+    this.main.appendChild(this.selectionHandles.topLeftHitArea);
+    this.main.appendChild(this.selectionHandles.bottomRightHitArea);
+  }
+
+  isPartRange(row, col) {
+    const areaSelection = this.wot.selections.createOrGetArea();
+
+    if (areaSelection.cellRange) {
+      if (row !== areaSelection.cellRange.to.row || col !== areaSelection.cellRange.to.col) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  updateMultipleSelectionHandlesPosition(row, col, top, left, width, height) {
+    const handleWidth = parseInt(this.selectionHandles.styles.topLeft.width, 10);
+    const hitAreaWidth = parseInt(this.selectionHandles.styles.topLeftHitArea.width, 10);
+
+    this.selectionHandles.styles.topLeft.top = `${parseInt(top - handleWidth, 10)}px`;
+    this.selectionHandles.styles.topLeft.left = `${parseInt(left - handleWidth, 10)}px`;
+
+    this.selectionHandles.styles.topLeftHitArea.top = `${parseInt(top - ((hitAreaWidth / 4) * 3), 10)}px`;
+    this.selectionHandles.styles.topLeftHitArea.left = `${parseInt(left - ((hitAreaWidth / 4) * 3), 10)}px`;
+
+    this.selectionHandles.styles.bottomRight.top = `${parseInt(top + height, 10)}px`;
+    this.selectionHandles.styles.bottomRight.left = `${parseInt(left + width, 10)}px`;
+
+    this.selectionHandles.styles.bottomRightHitArea.top = `${parseInt(top + height - (hitAreaWidth / 4), 10)}px`;
+    this.selectionHandles.styles.bottomRightHitArea.left = `${parseInt(left + width - (hitAreaWidth / 4), 10)}px`;
+
+    if (this.settings.border.cornerVisible && this.settings.border.cornerVisible()) {
+      this.selectionHandles.styles.topLeft.display = 'block';
+      this.selectionHandles.styles.topLeftHitArea.display = 'block';
+
+      if (this.isPartRange(row, col)) {
+        this.selectionHandles.styles.bottomRight.display = 'none';
+        this.selectionHandles.styles.bottomRightHitArea.display = 'none';
+      } else {
+        this.selectionHandles.styles.bottomRight.display = 'block';
+        this.selectionHandles.styles.bottomRightHitArea.display = 'block';
+      }
+    } else {
+      this.selectionHandles.styles.topLeft.display = 'none';
+      this.selectionHandles.styles.bottomRight.display = 'none';
+      this.selectionHandles.styles.topLeftHitArea.display = 'none';
+      this.selectionHandles.styles.bottomRightHitArea.display = 'none';
+    }
+
+    if (row === this.wot.wtSettings.getSetting('fixedRowsTop') || col === this.wot.wtSettings.getSetting('fixedColumnsLeft')) {
+      this.selectionHandles.styles.topLeft.zIndex = '9999';
+      this.selectionHandles.styles.topLeftHitArea.zIndex = '9999';
+    } else {
+      this.selectionHandles.styles.topLeft.zIndex = '';
+      this.selectionHandles.styles.topLeftHitArea.zIndex = '';
+    }
+  }
+
+  /**
+   * Show border around one or many cells
+   *
+   * @param {Array} corners
+   */
+  appear(corners) {
+    if (this.disabled) {
+      return;
+    }
+
+    let fromRow;
+    let toRow;
+    let fromColumn;
+    let toColumn;
+
+    const rowsCount = this.wot.wtTable.getRenderedRowsCount();
+
+    for (let i = 0; i < rowsCount; i += 1) {
+      const s = this.wot.wtTable.rowFilter.renderedToSource(i);
+
+      if (s >= corners[0] && s <= corners[2]) {
+        fromRow = s;
+        break;
+      }
+    }
+
+    for (let i = rowsCount - 1; i >= 0; i -= 1) {
+      const s = this.wot.wtTable.rowFilter.renderedToSource(i);
+
+      if (s >= corners[0] && s <= corners[2]) {
+        toRow = s;
+        break;
+      }
+    }
+
+    const columnsCount = this.wot.wtTable.getRenderedColumnsCount();
+
+    for (let i = 0; i < columnsCount; i += 1) {
+      const s = this.wot.wtTable.columnFilter.renderedToSource(i);
+
+      if (s >= corners[1] && s <= corners[3]) {
+        fromColumn = s;
+        break;
+      }
+    }
+
+    for (let i = columnsCount - 1; i >= 0; i -= 1) {
+      const s = this.wot.wtTable.columnFilter.renderedToSource(i);
+
+      if (s >= corners[1] && s <= corners[3]) {
+        toColumn = s;
+        break;
+      }
+    }
+    if (fromRow === void 0 || fromColumn === void 0) {
+      this.disappear();
+
+      return;
+    }
+    let fromTD = this.wot.wtTable.getCell(new CellCoords(fromRow, fromColumn));
+    const isMultiple = (fromRow !== toRow || fromColumn !== toColumn);
+    const toTD = isMultiple ? this.wot.wtTable.getCell(new CellCoords(toRow, toColumn)) : fromTD;
+    const fromOffset = offset(fromTD);
+    const toOffset = isMultiple ? offset(toTD) : fromOffset;
+    const containerOffset = offset(this.wot.wtTable.TABLE);
+    const minTop = fromOffset.top;
+    const minLeft = fromOffset.left;
+
+    let left = minLeft - containerOffset.left - 1;
+    let width = toOffset.left + outerWidth(toTD) - minLeft;
+
+    if (this.isEntireColumnSelected(fromRow, toRow)) {
+      const modifiedValues = this.getDimensionsFromHeader('columns', fromColumn, toColumn, containerOffset);
+      let fromTH = null;
+
+      if (modifiedValues) {
+        [fromTH, left, width] = modifiedValues;
+      }
+
+      if (fromTH) {
+        fromTD = fromTH;
+      }
+    }
+
+    let top = minTop - containerOffset.top - 1;
+    let height = toOffset.top + outerHeight(toTD) - minTop;
+
+    if (this.isEntireRowSelected(fromColumn, toColumn)) {
+      const modifiedValues = this.getDimensionsFromHeader('rows', fromRow, toRow, containerOffset);
+      let fromTH = null;
+
+      if (modifiedValues) {
+        [fromTH, top, height] = modifiedValues;
+      }
+
+      if (fromTH) {
+        fromTD = fromTH;
+      }
+    }
+
+    const style = getComputedStyle(fromTD);
+
+    if (parseInt(style.borderTopWidth, 10) > 0) {
+      top += 1;
+      height = height > 0 ? height - 1 : 0;
+    }
+    if (parseInt(style.borderLeftWidth, 10) > 0) {
+      left += 1;
+      width = width > 0 ? width - 1 : 0;
+    }
+
+    this.topStyle.top = `${top}px`;
+    this.topStyle.left = `${left}px`;
+    this.topStyle.width = `${width}px`;
+    this.topStyle.display = 'block';
+
+    this.leftStyle.top = `${top}px`;
+    this.leftStyle.left = `${left}px`;
+    this.leftStyle.height = `${height}px`;
+    this.leftStyle.display = 'block';
+
+    const delta = Math.floor(this.settings.border.width / 2);
+
+    this.bottomStyle.top = `${top + height - delta}px`;
+    this.bottomStyle.left = `${left}px`;
+    this.bottomStyle.width = `${width}px`;
+    this.bottomStyle.display = 'block';
+
+    this.rightStyle.top = `${top}px`;
+    this.rightStyle.left = `${left + width - delta}px`;
+    this.rightStyle.height = `${height + 1}px`;
+    this.rightStyle.display = 'block';
+
+    let cornerVisibleSetting = this.settings.border.cornerVisible;
+    cornerVisibleSetting = typeof cornerVisibleSetting === 'function' ? cornerVisibleSetting(this.settings.layerLevel) : cornerVisibleSetting;
+
+    const hookResult = this.wot.getSetting('onModifyGetCellCoords', toRow, toColumn);
+    let [checkRow, checkCol] = [toRow, toColumn];
+
+    if (hookResult && Array.isArray(hookResult)) {
+      [,, checkRow, checkCol] = hookResult;
+    }
+
+    if (isMobileBrowser() || !cornerVisibleSetting || this.isPartRange(checkRow, checkCol)) {
+      this.cornerStyle.display = 'none';
+
+    } else {
+      this.cornerStyle.top = `${top + height - 4}px`;
+      this.cornerStyle.left = `${left + width - 4}px`;
+      this.cornerStyle.borderRightWidth = this.cornerDefaultStyle.borderWidth;
+      this.cornerStyle.width = this.cornerDefaultStyle.width;
+
+      // Hide the fill handle, so the possible further adjustments won't force unneeded scrollbars.
+      this.cornerStyle.display = 'none';
+
+      let trimmingContainer = getTrimmingContainer(this.wot.wtTable.TABLE);
+      const trimToWindow = trimmingContainer === window;
+
+      if (trimToWindow) {
+        trimmingContainer = document.documentElement;
+      }
+
+      if (toColumn === this.wot.getSetting('totalColumns') - 1) {
+        const toTdOffsetLeft = trimToWindow ? toTD.getBoundingClientRect().left : toTD.offsetLeft;
+        const cornerRightEdge = toTdOffsetLeft + outerWidth(toTD) + (parseInt(this.cornerDefaultStyle.width, 10) / 2);
+        const cornerOverlappingContainer = cornerRightEdge >= innerWidth(trimmingContainer);
+
+        if (cornerOverlappingContainer) {
+          this.cornerStyle.left = `${Math.floor(left + width - 3 - (parseInt(this.cornerDefaultStyle.width, 10) / 2))}px`;
+          this.cornerStyle.borderRightWidth = 0;
+        }
+      }
+
+      if (toRow === this.wot.getSetting('totalRows') - 1) {
+        const toTdOffsetTop = trimToWindow ? toTD.getBoundingClientRect().top : toTD.offsetTop;
+        const cornerBottomEdge = toTdOffsetTop + outerHeight(toTD) + (parseInt(this.cornerDefaultStyle.height, 10) / 2);
+        const cornerOverlappingContainer = cornerBottomEdge >= innerHeight(trimmingContainer);
+
+        if (cornerOverlappingContainer) {
+          this.cornerStyle.top = `${Math.floor(top + height - 3 - (parseInt(this.cornerDefaultStyle.height, 10) / 2))}px`;
+          this.cornerStyle.borderBottomWidth = 0;
+        }
+      }
+
+      this.cornerStyle.display = 'block';
+    }
+
+    if (isMobileBrowser()) {
+      this.updateMultipleSelectionHandlesPosition(toRow, toColumn, top, left, width, height);
+    }
+  }
+
+  /**
+   * Check whether an entire column of cells is selected.
+   *
+   * @private
+   * @param {Number} startRowIndex Start row index.
+   * @param {Number} endRowIndex End row index.
+   */
+  isEntireColumnSelected(startRowIndex, endRowIndex) {
+    return startRowIndex === this.wot.wtTable.getFirstRenderedRow() && endRowIndex === this.wot.wtTable.getLastRenderedRow();
+  }
+
+  /**
+   * Check whether an entire row of cells is selected.
+   *
+   * @private
+   * @param {Number} startColumnIndex Start column index.
+   * @param {Number} endColumnIndex End column index.
+   */
+  isEntireRowSelected(startColumnIndex, endColumnIndex) {
+    return startColumnIndex === this.wot.wtTable.getFirstRenderedColumn() && endColumnIndex === this.wot.wtTable.getLastRenderedColumn();
+  }
+
+  /**
+   * Get left/top index and width/height depending on the `direction` provided.
+   *
+   * @private
+   * @param {String} direction `rows` or `columns`, defines if an entire column or row is selected.
+   * @param {Number} fromIndex Start index of the selection.
+   * @param {Number} toIndex End index of the selection.
+   * @param {Number} containerOffset offset of the container.
+   * @return {Array|Boolean} Returns an array of [headerElement, left, width] or [headerElement, top, height], depending on `direction` (`false` in case of an error getting the headers).
+   */
+  getDimensionsFromHeader(direction, fromIndex, toIndex, containerOffset) {
+    const rootHotElement = this.wot.wtTable.wtRootElement.parentNode;
+    let getHeaderFn = null;
+    let dimensionFn = null;
+    let entireSelectionClassname = null;
+    let index = null;
+    let dimension = null;
+    let dimensionProperty = null;
+    let startHeader = null;
+    let endHeader = null;
+
+    switch (direction) {
+      case 'rows':
+        getHeaderFn = (...args) => this.wot.wtTable.getRowHeader(...args);
+        dimensionFn = (...args) => outerHeight(...args);
+        entireSelectionClassname = 'ht__selection--rows';
+        dimensionProperty = 'top';
+
+        break;
+
+      case 'columns':
+        getHeaderFn = (...args) => this.wot.wtTable.getColumnHeader(...args);
+        dimensionFn = (...args) => outerWidth(...args);
+        entireSelectionClassname = 'ht__selection--columns';
+        dimensionProperty = 'left';
+        break;
+      default:
+    }
+
+    if (rootHotElement.className.includes(entireSelectionClassname)) {
+      const columnHeaderLevelCount = this.wot.getSetting('columnHeaders').length;
+
+      startHeader = getHeaderFn(fromIndex, columnHeaderLevelCount - 1);
+      endHeader = getHeaderFn(toIndex, columnHeaderLevelCount - 1);
+
+      if (!startHeader || !endHeader) {
+        return false;
+      }
+
+      const startHeaderOffset = offset(startHeader);
+      const endOffset = offset(endHeader);
+
+      if (startHeader && endHeader) {
+        index = startHeaderOffset[dimensionProperty] - containerOffset[dimensionProperty] - 1;
+        dimension = endOffset[dimensionProperty] + dimensionFn(endHeader) - startHeaderOffset[dimensionProperty];
+      }
+
+      return [startHeader, index, dimension];
+    }
+
+    return false;
+  }
+
+  /**
+   * Change border style.
+   *
+   * @private
+   * @param {String} borderElement Coordinate where add/remove border: top, right, bottom, left.
+   */
+  changeBorderStyle(borderElement, border) {
+    const style = this[borderElement].style;
+    const borderStyle = border[borderElement];
+
+    if (!borderStyle || borderStyle.hide) {
+      addClass(this[borderElement], 'hidden');
+
+    } else {
+      if (hasClass(this[borderElement], 'hidden')) {
+        removeClass(this[borderElement], 'hidden');
+      }
+
+      style.backgroundColor = borderStyle.color;
+
+      if (borderElement === 'top' || borderElement === 'bottom') {
+        style.height = `${borderStyle.width}px`;
+      }
+
+      if (borderElement === 'right' || borderElement === 'left') {
+        style.width = `${borderStyle.width}px`;
+      }
+    }
+  }
+
+  /**
+   * Change border style to default.
+   *
+   * @private
+   * @param {HTMLElement} position
+   */
+  changeBorderToDefaultStyle(position) {
+    const defaultBorder = {
+      width: 1,
+      color: '#000',
+    };
+    const style = this[position].style;
+
+    style.backgroundColor = defaultBorder.color;
+    style.width = `${defaultBorder.width}px`;
+    style.height = `${defaultBorder.width}px`;
+  }
+
+  /**
+   * Toggle class 'hidden' to element.
+   *
+   * @private
+   * @param {String} borderElement Coordinate where add/remove border: top, right, bottom, left.
+   * @return {Boolean}
+   */
+  toggleHiddenClass(borderElement, remove) {
+    this.changeBorderToDefaultStyle(borderElement);
+
+    if (remove) {
+      addClass(this[borderElement], 'hidden');
+    } else {
+      removeClass(this[borderElement], 'hidden');
+    }
+  }
+
+  /**
+   * Hide border
+   */
+  disappear() {
+    this.topStyle.display = 'none';
+    this.leftStyle.display = 'none';
+    this.bottomStyle.display = 'none';
+    this.rightStyle.display = 'none';
+    this.cornerStyle.display = 'none';
+
+    if (isMobileBrowser()) {
+      this.selectionHandles.styles.topLeft.display = 'none';
+      this.selectionHandles.styles.bottomRight.display = 'none';
+    }
+  }
+}
+
+export default Border;

+ 280 - 0
handsontable/src/3rdparty/walkontable/src/calculator/viewportColumns.js

@@ -0,0 +1,280 @@
+const privatePool = new WeakMap();
+
+/**
+ * Calculates indexes of columns to render OR columns that are visible.
+ * To redo the calculation, you need to create a new calculator.
+ *
+ * @class ViewportColumnsCalculator
+ */
+class ViewportColumnsCalculator {
+  /**
+   * Default column width
+   *
+   * @type {Number}
+   */
+  static get DEFAULT_WIDTH() {
+    return 50;
+  }
+
+  /**
+   * @param {Number} viewportWidth Width of the viewport
+   * @param {Number} scrollOffset Current horizontal scroll position of the viewport
+   * @param {Number} totalColumns Total number of rows
+   * @param {Function} columnWidthFn Function that returns the width of the column at a given index (in px)
+   * @param {Function} overrideFn Function that changes calculated this.startRow, this.endRow (used by MergeCells plugin)
+   * @param {Boolean} onlyFullyVisible if `true`, only startRow and endRow will be indexes of rows that are fully in viewport
+   * @param {Boolean} stretchH
+   * @param {Function} [stretchingColumnWidthFn] Function that returns the new width of the stretched column.
+   */
+  constructor(viewportWidth, scrollOffset, totalColumns, columnWidthFn, overrideFn, onlyFullyVisible, stretchH,
+              stretchingColumnWidthFn = width => width) {
+    privatePool.set(this, {
+      viewportWidth,
+      scrollOffset,
+      totalColumns,
+      columnWidthFn,
+      overrideFn,
+      onlyFullyVisible,
+      stretchingColumnWidthFn,
+    });
+
+    /**
+     * Number of rendered/visible columns
+     *
+     * @type {Number}
+     */
+    this.count = 0;
+
+    /**
+     * Index of the first rendered/visible column (can be overwritten using overrideFn)
+     *
+     * @type {Number|null}
+     */
+    this.startColumn = null;
+
+    /**
+     * Index of the last rendered/visible column (can be overwritten using overrideFn)
+     *
+     * @type {null}
+     */
+    this.endColumn = null;
+
+    /**
+     * Position of the first rendered/visible column (in px)
+     *
+     * @type {Number|null}
+     */
+    this.startPosition = null;
+
+    this.stretchAllRatio = 0;
+    this.stretchLastWidth = 0;
+    this.stretch = stretchH;
+    this.totalTargetWidth = 0;
+    this.needVerifyLastColumnWidth = true;
+    this.stretchAllColumnsWidth = [];
+
+    this.calculate();
+  }
+
+  /**
+   * Calculates viewport
+   */
+  calculate() {
+    let sum = 0;
+    let needReverse = true;
+    const startPositions = [];
+    let columnWidth;
+
+    const priv = privatePool.get(this);
+    const onlyFullyVisible = priv.onlyFullyVisible;
+    const overrideFn = priv.overrideFn;
+    const scrollOffset = priv.scrollOffset;
+    const totalColumns = priv.totalColumns;
+    const viewportWidth = priv.viewportWidth;
+
+    for (let i = 0; i < totalColumns; i++) {
+      columnWidth = this._getColumnWidth(i);
+
+      if (sum <= scrollOffset && !onlyFullyVisible) {
+        this.startColumn = i;
+      }
+
+      // +1 pixel for row header width compensation for horizontal scroll > 0
+      const compensatedViewportWidth = scrollOffset > 0 ? viewportWidth + 1 : viewportWidth;
+
+      if (sum >= scrollOffset && sum + columnWidth <= scrollOffset + compensatedViewportWidth) {
+        if (this.startColumn === null || this.startColumn === void 0) {
+          this.startColumn = i;
+        }
+        this.endColumn = i;
+      }
+      startPositions.push(sum);
+      sum += columnWidth;
+
+      if (!onlyFullyVisible) {
+        this.endColumn = i;
+      }
+      if (sum >= scrollOffset + viewportWidth) {
+        needReverse = false;
+        break;
+      }
+    }
+
+    if (this.endColumn === totalColumns - 1 && needReverse) {
+      this.startColumn = this.endColumn;
+
+      while (this.startColumn > 0) {
+        const viewportSum = startPositions[this.endColumn] + columnWidth - startPositions[this.startColumn - 1];
+
+        if (viewportSum <= viewportWidth || !onlyFullyVisible) {
+          this.startColumn -= 1;
+        }
+        if (viewportSum > viewportWidth) {
+          break;
+        }
+      }
+    }
+
+    if (this.startColumn !== null && overrideFn) {
+      overrideFn(this);
+    }
+    this.startPosition = startPositions[this.startColumn];
+
+    if (this.startPosition === void 0) {
+      this.startPosition = null;
+    }
+    if (this.startColumn !== null) {
+      this.count = this.endColumn - this.startColumn + 1;
+    }
+  }
+
+  /**
+   * Recalculate columns stretching.
+   *
+   * @param {Number} totalWidth
+   */
+  refreshStretching(totalWidth) {
+    if (this.stretch === 'none') {
+      return;
+    }
+    let totalColumnsWidth = totalWidth;
+    this.totalTargetWidth = totalColumnsWidth;
+
+    const priv = privatePool.get(this);
+    const totalColumns = priv.totalColumns;
+    let sumAll = 0;
+
+    for (let i = 0; i < totalColumns; i++) {
+      const columnWidth = this._getColumnWidth(i);
+      const permanentColumnWidth = priv.stretchingColumnWidthFn(void 0, i);
+
+      if (typeof permanentColumnWidth === 'number') {
+        totalColumnsWidth -= permanentColumnWidth;
+      } else {
+        sumAll += columnWidth;
+      }
+    }
+    const remainingSize = totalColumnsWidth - sumAll;
+
+    if (this.stretch === 'all' && remainingSize > 0) {
+      this.stretchAllRatio = totalColumnsWidth / sumAll;
+      this.stretchAllColumnsWidth = [];
+      this.needVerifyLastColumnWidth = true;
+
+    } else if (this.stretch === 'last' && totalColumnsWidth !== Infinity) {
+      const columnWidth = this._getColumnWidth(totalColumns - 1);
+      const lastColumnWidth = remainingSize + columnWidth;
+
+      this.stretchLastWidth = lastColumnWidth >= 0 ? lastColumnWidth : columnWidth;
+    }
+  }
+
+  /**
+   * Get stretched column width based on stretchH (all or last) setting passed in handsontable instance.
+   *
+   * @param {Number} column
+   * @param {Number} baseWidth
+   * @returns {Number|null}
+   */
+  getStretchedColumnWidth(column, baseWidth) {
+    let result = null;
+
+    if (this.stretch === 'all' && this.stretchAllRatio !== 0) {
+      result = this._getStretchedAllColumnWidth(column, baseWidth);
+
+    } else if (this.stretch === 'last' && this.stretchLastWidth !== 0) {
+      result = this._getStretchedLastColumnWidth(column);
+    }
+
+    return result;
+  }
+
+  /**
+   * @param {Number} column
+   * @param {Number} baseWidth
+   * @returns {Number}
+   * @private
+   */
+  _getStretchedAllColumnWidth(column, baseWidth) {
+    let sumRatioWidth = 0;
+    const priv = privatePool.get(this);
+    const totalColumns = priv.totalColumns;
+
+    if (!this.stretchAllColumnsWidth[column]) {
+      const stretchedWidth = Math.round(baseWidth * this.stretchAllRatio);
+      const newStretchedWidth = priv.stretchingColumnWidthFn(stretchedWidth, column);
+
+      if (newStretchedWidth === void 0) {
+        this.stretchAllColumnsWidth[column] = stretchedWidth;
+      } else {
+        this.stretchAllColumnsWidth[column] = isNaN(newStretchedWidth) ? this._getColumnWidth(column) : newStretchedWidth;
+      }
+    }
+
+    if (this.stretchAllColumnsWidth.length === totalColumns && this.needVerifyLastColumnWidth) {
+      this.needVerifyLastColumnWidth = false;
+
+      for (let i = 0; i < this.stretchAllColumnsWidth.length; i++) {
+        sumRatioWidth += this.stretchAllColumnsWidth[i];
+      }
+      if (sumRatioWidth !== this.totalTargetWidth) {
+        this.stretchAllColumnsWidth[this.stretchAllColumnsWidth.length - 1] += this.totalTargetWidth - sumRatioWidth;
+      }
+    }
+
+    return this.stretchAllColumnsWidth[column];
+  }
+
+  /**
+   * @param {Number} column
+   * @returns {Number|null}
+   * @private
+   */
+  _getStretchedLastColumnWidth(column) {
+    const priv = privatePool.get(this);
+    const totalColumns = priv.totalColumns;
+
+    if (column === totalColumns - 1) {
+      return this.stretchLastWidth;
+    }
+
+    return null;
+  }
+
+  /**
+   * @param {Number} column Column index.
+   * @returns {Number}
+   * @private
+   */
+  _getColumnWidth(column) {
+    let width = privatePool.get(this).columnWidthFn(column);
+
+    if (isNaN(width)) {
+      width = ViewportColumnsCalculator.DEFAULT_WIDTH;
+    }
+
+    return width;
+  }
+}
+
+export default ViewportColumnsCalculator;

+ 150 - 0
handsontable/src/3rdparty/walkontable/src/calculator/viewportRows.js

@@ -0,0 +1,150 @@
+const privatePool = new WeakMap();
+
+/**
+ * Calculates indexes of rows to render OR rows that are visible.
+ * To redo the calculation, you need to create a new calculator.
+ *
+ * @class ViewportRowsCalculator
+ */
+class ViewportRowsCalculator {
+  /**
+   * Default row height
+   *
+   * @type {Number}
+   */
+  static get DEFAULT_HEIGHT() {
+    return 23;
+  }
+
+  /**
+   * @param {Number} viewportHeight Height of the viewport
+   * @param {Number} scrollOffset Current vertical scroll position of the viewport
+   * @param {Number} totalRows Total number of rows
+   * @param {Function} rowHeightFn Function that returns the height of the row at a given index (in px)
+   * @param {Function} overrideFn Function that changes calculated this.startRow, this.endRow (used by MergeCells plugin)
+   * @param {Boolean} onlyFullyVisible if `true`, only startRow and endRow will be indexes of rows that are fully in viewport
+   * @param {Number} horizontalScrollbarHeight
+   */
+  constructor(viewportHeight, scrollOffset, totalRows, rowHeightFn, overrideFn, onlyFullyVisible, horizontalScrollbarHeight) {
+    privatePool.set(this, {
+      viewportHeight,
+      scrollOffset,
+      totalRows,
+      rowHeightFn,
+      overrideFn,
+      onlyFullyVisible,
+      horizontalScrollbarHeight
+    });
+
+    /**
+     * Number of rendered/visible rows
+     *
+     * @type {Number}
+     */
+    this.count = 0;
+
+    /**
+     * Index of the first rendered/visible row (can be overwritten using overrideFn)
+     *
+     * @type {Number|null}
+     */
+    this.startRow = null;
+
+    /**
+     * Index of the last rendered/visible row (can be overwritten using overrideFn)
+     *
+     * @type {null}
+     */
+    this.endRow = null;
+
+    /**
+     * Position of the first rendered/visible row (in px)
+     *
+     * @type {Number|null}
+     */
+    this.startPosition = null;
+
+    this.calculate();
+  }
+
+  /**
+   * Calculates viewport
+   */
+  calculate() {
+    let sum = 0;
+    let needReverse = true;
+    const startPositions = [];
+
+    const priv = privatePool.get(this);
+    const onlyFullyVisible = priv.onlyFullyVisible;
+    const overrideFn = priv.overrideFn;
+    const rowHeightFn = priv.rowHeightFn;
+    const scrollOffset = priv.scrollOffset;
+    const totalRows = priv.totalRows;
+    const viewportHeight = priv.viewportHeight;
+    const horizontalScrollbarHeight = priv.horizontalScrollbarHeight || 0;
+    let rowHeight;
+
+    // Calculate the number (start and end index) of rows needed
+    for (let i = 0; i < totalRows; i++) {
+      rowHeight = rowHeightFn(i);
+
+      if (isNaN(rowHeight)) {
+        rowHeight = ViewportRowsCalculator.DEFAULT_HEIGHT;
+      }
+      if (sum <= scrollOffset && !onlyFullyVisible) {
+        this.startRow = i;
+      }
+
+      // the row is within the "visible range"
+      if (sum >= scrollOffset && sum + rowHeight <= scrollOffset + viewportHeight - horizontalScrollbarHeight) {
+        if (this.startRow === null) {
+          this.startRow = i;
+        }
+        this.endRow = i;
+      }
+      startPositions.push(sum);
+      sum += rowHeight;
+
+      if (!onlyFullyVisible) {
+        this.endRow = i;
+      }
+      if (sum >= scrollOffset + viewportHeight - horizontalScrollbarHeight) {
+        needReverse = false;
+        break;
+      }
+    }
+
+    // If the estimation has reached the last row and there is still some space available in the viewport,
+    // we need to render in reverse in order to fill the whole viewport with rows
+    if (this.endRow === totalRows - 1 && needReverse) {
+      this.startRow = this.endRow;
+
+      while (this.startRow > 0) {
+        // rowHeight is the height of the last row
+        const viewportSum = startPositions[this.endRow] + rowHeight - startPositions[this.startRow - 1];
+
+        if (viewportSum <= viewportHeight - horizontalScrollbarHeight || !onlyFullyVisible) {
+          this.startRow -= 1;
+        }
+        if (viewportSum >= viewportHeight - horizontalScrollbarHeight) {
+          break;
+        }
+      }
+    }
+
+    if (this.startRow !== null && overrideFn) {
+      overrideFn(this);
+    }
+    this.startPosition = startPositions[this.startRow];
+
+    if (this.startPosition === void 0) {
+      this.startPosition = null;
+    }
+    if (this.startRow !== null) {
+      this.count = this.endRow - this.startRow + 1;
+    }
+  }
+}
+
+export default ViewportRowsCalculator;

+ 113 - 0
handsontable/src/3rdparty/walkontable/src/cell/coords.js

@@ -0,0 +1,113 @@
+/**
+ * CellCoords holds cell coordinates (row, column) and few method to validate them and retrieve as an array or an object.
+ *
+ * @util
+ */
+class CellCoords {
+  constructor(row, column) {
+    /**
+     * Row index.
+     *
+     * @type {Number}
+     */
+    this.row = null;
+    /**
+     * Column index.
+     *
+     * @type {Number}
+     */
+    this.col = null;
+
+    if (typeof row !== 'undefined' && typeof column !== 'undefined') {
+      this.row = row;
+      this.col = column;
+    }
+  }
+
+  /**
+   * Checks if given set of coordinates is valid in context of a given Walkontable instance.
+   *
+   * @param {Walkontable} wot A Walkontable instance.
+   * @returns {Boolean}
+   */
+  isValid(wot) {
+    // is it a valid cell index (0 or higher)
+    if (this.row < 0 || this.col < 0) {
+      return false;
+    }
+    // is selection within total rows and columns
+    if (this.row >= wot.getSetting('totalRows') || this.col >= wot.getSetting('totalColumns')) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Checks if this cell coordinates are the same as cell coordinates given as an argument.
+   *
+   * @param {CellCoords} cellCoords Cell coordinates to equal.
+   * @returns {Boolean}
+   */
+  isEqual(cellCoords) {
+    if (cellCoords === this) {
+      return true;
+    }
+
+    return this.row === cellCoords.row && this.col === cellCoords.col;
+  }
+
+  /**
+   * Checks if tested coordinates are positioned in south-east from this cell coordinates.
+   *
+   * @param {Object} testedCoords Cell coordinates to check.
+   * @returns {Boolean}
+   */
+  isSouthEastOf(testedCoords) {
+    return this.row >= testedCoords.row && this.col >= testedCoords.col;
+  }
+
+  /**
+   * Checks if tested coordinates are positioned in north-east from this cell coordinates.
+   *
+   * @param {Object} testedCoords Cell coordinates to check.
+   * @returns {Boolean}
+   */
+  isNorthWestOf(testedCoords) {
+    return this.row <= testedCoords.row && this.col <= testedCoords.col;
+  }
+
+  /**
+   * Checks if tested coordinates are positioned in south-west from this cell coordinates.
+   *
+   * @param {Object} testedCoords Cell coordinates to check.
+   * @returns {Boolean}
+   */
+  isSouthWestOf(testedCoords) {
+    return this.row >= testedCoords.row && this.col <= testedCoords.col;
+  }
+
+  /**
+   * Checks if tested coordinates are positioned in north-east from this cell coordinates.
+   *
+   * @param {Object} testedCoords Cell coordinates to check.
+   * @returns {Boolean}
+   */
+  isNorthEastOf(testedCoords) {
+    return this.row <= testedCoords.row && this.col >= testedCoords.col;
+  }
+
+  /**
+   * Converts CellCoords to literal object with `row` and `col` properties.
+   *
+   * @return {Object} Returns a literal object with `row` and `col` properties.
+   */
+  toObject() {
+    return {
+      row: this.row,
+      col: this.col,
+    };
+  }
+}
+
+export default CellCoords;

+ 585 - 0
handsontable/src/3rdparty/walkontable/src/cell/range.js

@@ -0,0 +1,585 @@
+import CellCoords from './../cell/coords';
+
+/**
+ * CellRange holds cell coordinates as {@link CellCoords} instances. This object represent unit of the selection layer which
+ * can contains multiple contiquous cells or single cell.
+ *
+ * @util
+ */
+class CellRange {
+  constructor(highlight, from = highlight, to = highlight) {
+    /**
+     * Used to draw bold border around a cell where selection was started and to edit the cell when you press Enter.
+     *
+     * @type {CellCoords}
+     */
+    this.highlight = highlight;
+    /**
+     * Usually the same as highlight, but in Excel there is distinction - one can change highlight within a selection.
+     *
+     * @type {CellCoords}
+     */
+    this.from = from;
+    /**
+     * End selection.
+     *
+     * @type {CellCoords}
+     */
+    this.to = to;
+  }
+
+  /**
+   * Set the new coordinates for highlighting selection.
+   *
+   * @param {CellCoords} coords Coordinates to use.
+   */
+  setHighlight(coords) {
+    this.highlight = coords;
+
+    return this;
+  }
+
+  /**
+   * Set the new coordinates where selection starts from.
+   *
+   * @param {CellCoords} coords Coordinates to use.
+   */
+  setFrom(coords) {
+    this.from = coords;
+
+    return this;
+  }
+
+  /**
+   * Set new coordinates where selection ends from.
+   *
+   * @param {CellCoords} coords Coordinates to use.
+   */
+  setTo(coords) {
+    this.to = coords;
+
+    return this;
+  }
+
+  /**
+   * Checks if given coordinates are valid in context of a given Walkontable instance.
+   *
+   * @param {Walkontable} wot The Walkontable instance.
+   * @returns {Boolean}
+   */
+  isValid(wot) {
+    return this.from.isValid(wot) && this.to.isValid(wot);
+  }
+
+  /**
+   * Checks if this cell range is restricted to one cell
+   *
+   * @returns {Boolean}
+   */
+  isSingle() {
+    return this.from.row === this.to.row && this.from.col === this.to.col;
+  }
+
+  /**
+   * Returns selected range height (in number of rows).
+   *
+   * @returns {Number}
+   */
+  getHeight() {
+    return Math.max(this.from.row, this.to.row) - Math.min(this.from.row, this.to.row) + 1;
+  }
+
+  /**
+   * Returns selected range width (in number of columns).
+   *
+   * @returns {Number}
+   */
+  getWidth() {
+    return Math.max(this.from.col, this.to.col) - Math.min(this.from.col, this.to.col) + 1;
+  }
+
+  /**
+   * Checks if given cell coordinates are within `from` and `to` cell coordinates of this range.
+   *
+   * @param {CellCoords} cellCoords The cell coordinates to check.
+   * @returns {Boolean}
+   */
+  includes(cellCoords) {
+    const { row, col } = cellCoords;
+    const topLeft = this.getTopLeftCorner();
+    const bottomRight = this.getBottomRightCorner();
+
+    return topLeft.row <= row && bottomRight.row >= row && topLeft.col <= col && bottomRight.col >= col;
+  }
+
+  /**
+   * Checks if given range is within of this range.
+   *
+   * @param {CellRange} cellRange The cells range to check.
+   * @returns {Boolean}
+   */
+  includesRange(cellRange) {
+    return this.includes(cellRange.getTopLeftCorner()) && this.includes(cellRange.getBottomRightCorner());
+  }
+
+  /**
+   * Checks if given range is equal to this range.
+   *
+   * @param {CellRange} cellRange The cells range to check.
+   * @returns {Boolean}
+   */
+  isEqual(cellRange) {
+    return (Math.min(this.from.row, this.to.row) === Math.min(cellRange.from.row, cellRange.to.row)) &&
+      (Math.max(this.from.row, this.to.row) === Math.max(cellRange.from.row, cellRange.to.row)) &&
+      (Math.min(this.from.col, this.to.col) === Math.min(cellRange.from.col, cellRange.to.col)) &&
+      (Math.max(this.from.col, this.to.col) === Math.max(cellRange.from.col, cellRange.to.col));
+  }
+
+  /**
+   * Checks if tested range overlaps with the range. Range A is considered to to be overlapping with range B
+   * if intersection of A and B or B and A is not empty.
+   *
+   * @param {CellRange} cellRange The cells range to check.
+   * @returns {Boolean}
+   */
+  overlaps(cellRange) {
+    return cellRange.isSouthEastOf(this.getTopLeftCorner()) && cellRange.isNorthWestOf(this.getBottomRightCorner());
+  }
+
+  /**
+   * Checks if tested coordinates are positioned in south-east from this cell range.
+   *
+   * @param {CellRange} cellRange The cells range to check.
+   * @returns {Boolean}
+   */
+  isSouthEastOf(cellRange) {
+    return this.getTopLeftCorner().isSouthEastOf(cellRange) || this.getBottomRightCorner().isSouthEastOf(cellRange);
+  }
+
+  /**
+   * Checks if tested coordinates are positioned in north-west from this cell range.
+   *
+   * @param {CellRange} cellRange The cells range to check.
+   * @returns {Boolean}
+   */
+  isNorthWestOf(cellRange) {
+    return this.getTopLeftCorner().isNorthWestOf(cellRange) || this.getBottomRightCorner().isNorthWestOf(cellRange);
+  }
+
+  /**
+   * Returns `true` if the provided range is overlapping the current range horizontally (e.g. the current range's last
+   * column is 5 and the provided range's first column is 3).
+   *
+   * @param {CellRange} cellRange The cells range to check.
+   * @returns {Boolean}
+   */
+  isOverlappingHorizontally(cellRange) {
+    return (this.getTopRightCorner().col >= cellRange.getTopLeftCorner().col && this.getTopRightCorner().col <= cellRange.getTopRightCorner().col)
+      || (this.getTopLeftCorner().col <= cellRange.getTopRightCorner().col && this.getTopLeftCorner().col >= cellRange.getTopLeftCorner().col);
+  }
+
+  /**
+   * Returns `true` if the provided range is overlapping the current range vertically (e.g. the current range's last
+   *  row is 5 and the provided range's first row is 3).
+   *
+   * @param {CellRange} cellRange The cells range to check.
+   * @returns {Boolean}
+   */
+  isOverlappingVertically(cellRange) {
+    return (this.getBottomRightCorner().row >= cellRange.getTopRightCorner().row && this.getBottomRightCorner().row <= cellRange.getBottomRightCorner().row)
+      || (this.getTopRightCorner().row <= cellRange.getBottomRightCorner().row && this.getTopRightCorner().row >= cellRange.getTopRightCorner().row);
+  }
+
+  /**
+   * Adds a cell to a range (only if exceeds corners of the range). Returns information if range was expanded.
+   *
+   * @param {CellCoords} cellCoords
+   * @returns {Boolean}
+   */
+  expand(cellCoords) {
+    const topLeft = this.getTopLeftCorner();
+    const bottomRight = this.getBottomRightCorner();
+
+    if (cellCoords.row < topLeft.row || cellCoords.col < topLeft.col ||
+      cellCoords.row > bottomRight.row || cellCoords.col > bottomRight.col) {
+      this.from = new CellCoords(Math.min(topLeft.row, cellCoords.row), Math.min(topLeft.col, cellCoords.col));
+      this.to = new CellCoords(Math.max(bottomRight.row, cellCoords.row), Math.max(bottomRight.col, cellCoords.col));
+
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Expand the current object by the range passed in the first argument.
+   *
+   * @param {CellRange} expandingRange Object extending the range.
+   * @returns {Boolean}
+   */
+  expandByRange(expandingRange) {
+    if (this.includesRange(expandingRange) || !this.overlaps(expandingRange)) {
+      return false;
+    }
+
+    const topLeft = this.getTopLeftCorner();
+    const bottomRight = this.getBottomRightCorner();
+    const initialDirection = this.getDirection();
+
+    const expandingTopLeft = expandingRange.getTopLeftCorner();
+    const expandingBottomRight = expandingRange.getBottomRightCorner();
+
+    const resultTopRow = Math.min(topLeft.row, expandingTopLeft.row);
+    const resultTopCol = Math.min(topLeft.col, expandingTopLeft.col);
+    const resultBottomRow = Math.max(bottomRight.row, expandingBottomRight.row);
+    const resultBottomCol = Math.max(bottomRight.col, expandingBottomRight.col);
+
+    const finalFrom = new CellCoords(resultTopRow, resultTopCol);
+    const finalTo = new CellCoords(resultBottomRow, resultBottomCol);
+
+    this.from = finalFrom;
+    this.to = finalTo;
+
+    this.setDirection(initialDirection);
+
+    if (this.highlight.row === this.getBottomRightCorner().row && this.getVerticalDirection() === 'N-S') {
+      this.flipDirectionVertically();
+    }
+
+    if (this.highlight.col === this.getTopRightCorner().col && this.getHorizontalDirection() === 'W-E') {
+      this.flipDirectionHorizontally();
+    }
+
+    return true;
+  }
+
+  /**
+   * Gets the direction of the selection.
+   *
+   * @returns {String} Returns one of the values: `'NW-SE'`, `'NE-SW'`, `'SE-NW'`, `'SW-NE'`.
+   */
+  getDirection() {
+    if (this.from.isNorthWestOf(this.to)) { // NorthWest - SouthEast
+      return 'NW-SE';
+
+    } else if (this.from.isNorthEastOf(this.to)) { // NorthEast - SouthWest
+      return 'NE-SW';
+
+    } else if (this.from.isSouthEastOf(this.to)) { // SouthEast - NorthWest
+      return 'SE-NW';
+
+    } else if (this.from.isSouthWestOf(this.to)) { // SouthWest - NorthEast
+      return 'SW-NE';
+    }
+  }
+
+  /**
+   * Sets the direction of the selection.
+   *
+   * @param {String} direction One of the values: `'NW-SE'`, `'NE-SW'`, `'SE-NW'`, `'SW-NE'`.
+   */
+  setDirection(direction) {
+    switch (direction) {
+      case 'NW-SE':
+        [this.from, this.to] = [this.getTopLeftCorner(), this.getBottomRightCorner()];
+        break;
+      case 'NE-SW':
+        [this.from, this.to] = [this.getTopRightCorner(), this.getBottomLeftCorner()];
+        break;
+      case 'SE-NW':
+        [this.from, this.to] = [this.getBottomRightCorner(), this.getTopLeftCorner()];
+        break;
+      case 'SW-NE':
+        [this.from, this.to] = [this.getBottomLeftCorner(), this.getTopRightCorner()];
+        break;
+      default:
+        break;
+    }
+  }
+
+  /**
+   * Gets the vertical direction of the range.
+   *
+   * @returns {String} Returns one of the values: `N-S` (north->south), `S-N` (south->north).
+   */
+  getVerticalDirection() {
+    return ['NE-SW', 'NW-SE'].indexOf(this.getDirection()) > -1 ? 'N-S' : 'S-N';
+  }
+
+  /**
+   * Gets the horizontal direction of the range.
+   *
+   * @returns {String} Returns one of the values: `W-E` (west->east), `E-W` (east->west).
+   */
+  getHorizontalDirection() {
+    return ['NW-SE', 'SW-NE'].indexOf(this.getDirection()) > -1 ? 'W-E' : 'E-W';
+  }
+
+  /**
+   * Flip the direction vertically. (e.g. `NW-SE` changes to `SW-NE`).
+   */
+  flipDirectionVertically() {
+    const direction = this.getDirection();
+    switch (direction) {
+      case 'NW-SE':
+        this.setDirection('SW-NE');
+        break;
+      case 'NE-SW':
+        this.setDirection('SE-NW');
+        break;
+      case 'SE-NW':
+        this.setDirection('NE-SW');
+        break;
+      case 'SW-NE':
+        this.setDirection('NW-SE');
+        break;
+      default:
+        break;
+    }
+  }
+
+  /**
+   * Flip the direction horizontally. (e.g. `NW-SE` changes to `NE-SW`).
+   */
+  flipDirectionHorizontally() {
+    const direction = this.getDirection();
+    switch (direction) {
+      case 'NW-SE':
+        this.setDirection('NE-SW');
+        break;
+      case 'NE-SW':
+        this.setDirection('NW-SE');
+        break;
+      case 'SE-NW':
+        this.setDirection('SW-NE');
+        break;
+      case 'SW-NE':
+        this.setDirection('SE-NW');
+        break;
+      default:
+        break;
+    }
+  }
+
+  /**
+   * Gets the top left corner of this range.
+   *
+   * @returns {CellCoords}
+   */
+  getTopLeftCorner() {
+    return new CellCoords(Math.min(this.from.row, this.to.row), Math.min(this.from.col, this.to.col));
+  }
+
+  /**
+   * Gets the bottom right corner of this range.
+   *
+   * @returns {CellCoords}
+   */
+  getBottomRightCorner() {
+    return new CellCoords(Math.max(this.from.row, this.to.row), Math.max(this.from.col, this.to.col));
+  }
+
+  /**
+   * Gets the top right corner of this range.
+   *
+   * @returns {CellCoords}
+   */
+  getTopRightCorner() {
+    return new CellCoords(Math.min(this.from.row, this.to.row), Math.max(this.from.col, this.to.col));
+  }
+
+  /**
+   * Gets the bottom left corner of this range.
+   *
+   * @returns {CellCoords}
+   */
+  getBottomLeftCorner() {
+    return new CellCoords(Math.max(this.from.row, this.to.row), Math.min(this.from.col, this.to.col));
+  }
+
+  /**
+   * Checks if coordinates match to one of the 4th corners of this range.
+   *
+   * @param {CellCoords} coords Cell coordinates to check.
+   * @param {CellRange} [expandedRange]
+   * @returns {Boolean}
+   */
+  isCorner(coords, expandedRange) {
+    if (expandedRange && expandedRange.includes(coords) &&
+       (this.getTopLeftCorner().isEqual(new CellCoords(expandedRange.from.row, expandedRange.from.col)) ||
+       this.getTopRightCorner().isEqual(new CellCoords(expandedRange.from.row, expandedRange.to.col)) ||
+       this.getBottomLeftCorner().isEqual(new CellCoords(expandedRange.to.row, expandedRange.from.col)) ||
+       this.getBottomRightCorner().isEqual(new CellCoords(expandedRange.to.row, expandedRange.to.col)))) {
+      return true;
+    }
+
+    return coords.isEqual(this.getTopLeftCorner()) || coords.isEqual(this.getTopRightCorner()) ||
+      coords.isEqual(this.getBottomLeftCorner()) || coords.isEqual(this.getBottomRightCorner());
+  }
+
+  /**
+   * Gets coordinates of the corner which is opposite to the matched. When the passed coordinates matched to the
+   * bottom-right corner of this range then the coordinates for top-left will be returned.
+   *
+   * @param {CellCoords} coords Cell coordinates to check.
+   * @param {CellRange} [expandedRange]
+   * @returns {CellCoords}
+   */
+  getOppositeCorner(coords, expandedRange) {
+    if (!(coords instanceof CellCoords)) {
+      return false;
+    }
+
+    if (expandedRange) {
+      if (expandedRange.includes(coords)) {
+        if (this.getTopLeftCorner().isEqual(new CellCoords(expandedRange.from.row, expandedRange.from.col))) {
+          return this.getBottomRightCorner();
+        }
+        if (this.getTopRightCorner().isEqual(new CellCoords(expandedRange.from.row, expandedRange.to.col))) {
+          return this.getBottomLeftCorner();
+        }
+        if (this.getBottomLeftCorner().isEqual(new CellCoords(expandedRange.to.row, expandedRange.from.col))) {
+          return this.getTopRightCorner();
+        }
+        if (this.getBottomRightCorner().isEqual(new CellCoords(expandedRange.to.row, expandedRange.to.col))) {
+          return this.getTopLeftCorner();
+        }
+      }
+    }
+
+    if (coords.isEqual(this.getBottomRightCorner())) {
+      return this.getTopLeftCorner();
+
+    } else if (coords.isEqual(this.getTopLeftCorner())) {
+      return this.getBottomRightCorner();
+
+    } else if (coords.isEqual(this.getTopRightCorner())) {
+      return this.getBottomLeftCorner();
+
+    } else if (coords.isEqual(this.getBottomLeftCorner())) {
+      return this.getTopRightCorner();
+    }
+  }
+
+  /**
+   * @param {CellRange} range
+   * @returns {Array}
+   */
+  getBordersSharedWith(range) {
+    if (!this.includesRange(range)) {
+      return [];
+    }
+
+    const thisBorders = {
+      top: Math.min(this.from.row, this.to.row),
+      bottom: Math.max(this.from.row, this.to.row),
+      left: Math.min(this.from.col, this.to.col),
+      right: Math.max(this.from.col, this.to.col)
+    };
+    const rangeBorders = {
+      top: Math.min(range.from.row, range.to.row),
+      bottom: Math.max(range.from.row, range.to.row),
+      left: Math.min(range.from.col, range.to.col),
+      right: Math.max(range.from.col, range.to.col)
+    };
+    const result = [];
+
+    if (thisBorders.top === rangeBorders.top) {
+      result.push('top');
+    }
+    if (thisBorders.right === rangeBorders.right) {
+      result.push('right');
+    }
+    if (thisBorders.bottom === rangeBorders.bottom) {
+      result.push('bottom');
+    }
+    if (thisBorders.left === rangeBorders.left) {
+      result.push('left');
+    }
+
+    return result;
+  }
+
+  /**
+   * Get inner selected cell coords defined by this range
+   *
+   * @returns {Array}
+   */
+  getInner() {
+    const topLeft = this.getTopLeftCorner();
+    const bottomRight = this.getBottomRightCorner();
+    const out = [];
+
+    for (let r = topLeft.row; r <= bottomRight.row; r++) {
+      for (let c = topLeft.col; c <= bottomRight.col; c++) {
+        if (!(this.from.row === r && this.from.col === c) && !(this.to.row === r && this.to.col === c)) {
+          out.push(new CellCoords(r, c));
+        }
+      }
+    }
+    return out;
+  }
+
+  /**
+   * Get all selected cell coords defined by this range
+   *
+   * @returns {Array}
+   */
+  getAll() {
+    const topLeft = this.getTopLeftCorner();
+    const bottomRight = this.getBottomRightCorner();
+    const out = [];
+
+    for (let r = topLeft.row; r <= bottomRight.row; r++) {
+      for (let c = topLeft.col; c <= bottomRight.col; c++) {
+        if (topLeft.row === r && topLeft.col === c) {
+          out.push(topLeft);
+
+        } else if (bottomRight.row === r && bottomRight.col === c) {
+          out.push(bottomRight);
+
+        } else {
+          out.push(new CellCoords(r, c));
+        }
+      }
+    }
+
+    return out;
+  }
+
+  /**
+   * Runs a callback function against all cells in the range. You can break the iteration by returning
+   * `false` in the callback function
+   *
+   * @param callback {Function}
+   */
+  forAll(callback) {
+    const topLeft = this.getTopLeftCorner();
+    const bottomRight = this.getBottomRightCorner();
+
+    for (let r = topLeft.row; r <= bottomRight.row; r++) {
+      for (let c = topLeft.col; c <= bottomRight.col; c++) {
+        const breakIteration = callback(r, c);
+
+        if (breakIteration === false) {
+          return;
+        }
+      }
+    }
+  }
+
+  /**
+   * Convert CellRange to literal object.
+   *
+   * @return {Object} Returns a literal object with `from` and `to` properties which each of that object
+   *                  contains `row` and `col` keys.
+   */
+  toObject() {
+    return {
+      from: this.from.toObject(),
+      to: this.to.toObject(),
+    };
+  }
+}
+
+export default CellRange;

+ 267 - 0
handsontable/src/3rdparty/walkontable/src/core.js

@@ -0,0 +1,267 @@
+import {
+  addClass,
+  fastInnerText,
+  isVisible,
+  removeClass,
+} from './../../../helpers/dom/element';
+import { objectEach } from './../../../helpers/object';
+import { toUpperCaseFirst, randomString } from './../../../helpers/string';
+import Event from './event';
+import Overlays from './overlays';
+import Scroll from './scroll';
+import Settings from './settings';
+import Table from './table';
+import Viewport from './viewport';
+
+/**
+ * @class Walkontable
+ */
+class Walkontable {
+  /**
+   * @param {Object} settings
+   */
+  constructor(settings) {
+    const originalHeaders = [];
+
+    // this is the namespace for global events
+    this.guid = `wt_${randomString()}`;
+
+    // bootstrap from settings
+    if (settings.cloneSource) {
+      this.cloneSource = settings.cloneSource;
+      this.cloneOverlay = settings.cloneOverlay;
+      this.wtSettings = settings.cloneSource.wtSettings;
+      this.wtTable = new Table(this, settings.table, settings.wtRootElement);
+      this.wtScroll = new Scroll(this);
+      this.wtViewport = settings.cloneSource.wtViewport;
+      this.wtEvent = new Event(this);
+      this.selections = this.cloneSource.selections;
+    } else {
+      this.wtSettings = new Settings(this, settings);
+      this.wtTable = new Table(this, settings.table);
+      this.wtScroll = new Scroll(this);
+      this.wtViewport = new Viewport(this);
+      this.wtEvent = new Event(this);
+      this.selections = this.getSetting('selections');
+      this.wtOverlays = new Overlays(this);
+      this.exportSettingsAsClassNames();
+    }
+
+    // find original headers
+    if (this.wtTable.THEAD.childNodes.length && this.wtTable.THEAD.childNodes[0].childNodes.length) {
+      for (let c = 0, clen = this.wtTable.THEAD.childNodes[0].childNodes.length; c < clen; c++) {
+        originalHeaders.push(this.wtTable.THEAD.childNodes[0].childNodes[c].innerHTML);
+      }
+      if (!this.getSetting('columnHeaders').length) {
+        this.update('columnHeaders', [
+          function(column, TH) {
+            fastInnerText(TH, originalHeaders[column]);
+          }
+        ]);
+      }
+    }
+    this.drawn = false;
+    this.drawInterrupted = false;
+  }
+
+  /**
+   * Force rerender of Walkontable
+   *
+   * @param {Boolean} [fastDraw=false] When `true`, try to refresh only the positions of borders without rerendering
+   *                                   the data. It will only work if Table.draw() does not force
+   *                                   rendering anyway
+   * @returns {Walkontable}
+   */
+  draw(fastDraw = false) {
+    this.drawInterrupted = false;
+
+    if (!fastDraw && !isVisible(this.wtTable.TABLE)) {
+      // draw interrupted because TABLE is not visible
+      this.drawInterrupted = true;
+    } else {
+      this.wtTable.draw(fastDraw);
+    }
+
+    return this;
+  }
+
+  /**
+   * Returns the TD at coords. If topmost is set to true, returns TD from the topmost overlay layer,
+   * if not set or set to false, returns TD from the master table.
+   *
+   * @param {CellCoords} coords
+   * @param {Boolean} [topmost=false]
+   * @returns {Object}
+   */
+  getCell(coords, topmost = false) {
+    if (!topmost) {
+      return this.wtTable.getCell(coords);
+    }
+
+    const totalRows = this.wtSettings.getSetting('totalRows');
+    const fixedRowsTop = this.wtSettings.getSetting('fixedRowsTop');
+    const fixedRowsBottom = this.wtSettings.getSetting('fixedRowsBottom');
+    const fixedColumns = this.wtSettings.getSetting('fixedColumnsLeft');
+
+    if (coords.row < fixedRowsTop && coords.col < fixedColumns) {
+      return this.wtOverlays.topLeftCornerOverlay.clone.wtTable.getCell(coords);
+
+    } else if (coords.row < fixedRowsTop) {
+      return this.wtOverlays.topOverlay.clone.wtTable.getCell(coords);
+
+    } else if (coords.col < fixedColumns && coords.row >= totalRows - fixedRowsBottom) {
+      if (this.wtOverlays.bottomLeftCornerOverlay && this.wtOverlays.bottomLeftCornerOverlay.clone) {
+        return this.wtOverlays.bottomLeftCornerOverlay.clone.wtTable.getCell(coords);
+      }
+
+    } else if (coords.col < fixedColumns) {
+      return this.wtOverlays.leftOverlay.clone.wtTable.getCell(coords);
+
+    } else if (coords.row < totalRows && coords.row > totalRows - fixedRowsBottom) {
+      if (this.wtOverlays.bottomOverlay && this.wtOverlays.bottomOverlay.clone) {
+        return this.wtOverlays.bottomOverlay.clone.wtTable.getCell(coords);
+      }
+
+    }
+
+    return this.wtTable.getCell(coords);
+  }
+
+  /**
+   * @param {Object} settings
+   * @param {*} value
+   * @returns {Walkontable}
+   */
+  update(settings, value) {
+    return this.wtSettings.update(settings, value);
+  }
+
+  /**
+   * Scrolls the viewport to a cell (rerenders if needed).
+   *
+   * @param {CellCoords} coords
+   * @param {Boolean} [snapToTop]
+   * @param {Boolean} [snapToRight]
+   * @param {Boolean} [snapToBottom]
+   * @param {Boolean} [snapToLeft]
+   * @returns {Boolean}
+   */
+  scrollViewport(coords, snapToTop, snapToRight, snapToBottom, snapToLeft) {
+    return this.wtScroll.scrollViewport(coords, snapToTop, snapToRight, snapToBottom, snapToLeft);
+  }
+
+  /**
+   * Scrolls the viewport to a column (rerenders if needed).
+   *
+   * @param {Number} column Visual column index.
+   * @param {Boolean} [snapToRight]
+   * @param {Boolean} [snapToLeft]
+   * @returns {Boolean}
+   */
+  scrollViewportHorizontally(column, snapToRight, snapToLeft) {
+    return this.wtScroll.scrollViewportHorizontally(column, snapToRight, snapToLeft);
+  }
+
+  /**
+   * Scrolls the viewport to a row (rerenders if needed).
+   *
+   * @param {Number} row Visual row index.
+   * @param {Boolean} [snapToTop]
+   * @param {Boolean} [snapToBottom]
+   * @returns {Boolean}
+   */
+  scrollViewportVertically(row, snapToTop, snapToBottom) {
+    return this.wtScroll.scrollViewportVertically(row, snapToTop, snapToBottom);
+  }
+
+  /**
+   * @returns {Array}
+   */
+  getViewport() {
+    return [
+      this.wtTable.getFirstVisibleRow(),
+      this.wtTable.getFirstVisibleColumn(),
+      this.wtTable.getLastVisibleRow(),
+      this.wtTable.getLastVisibleColumn()
+    ];
+  }
+
+  /**
+   * Get overlay name
+   *
+   * @returns {String}
+   */
+  getOverlayName() {
+    return this.cloneOverlay ? this.cloneOverlay.type : 'master';
+  }
+
+  /**
+   * Check overlay type of this Walkontable instance.
+   *
+   * @param {String} name Clone type @see {Overlay.CLONE_TYPES}.
+   * @returns {Boolean}
+   */
+  isOverlayName(name) {
+    if (this.cloneOverlay) {
+      return this.cloneOverlay.type === name;
+    }
+
+    return false;
+  }
+
+  /**
+   * Export settings as class names added to the parent element of the table.
+   */
+  exportSettingsAsClassNames() {
+    const toExport = {
+      rowHeaders: ['array'],
+      columnHeaders: ['array']
+    };
+    const allClassNames = [];
+    const newClassNames = [];
+
+    objectEach(toExport, (optionType, key) => {
+      if (optionType.indexOf('array') > -1 && this.getSetting(key).length) {
+        newClassNames.push(`ht${toUpperCaseFirst(key)}`);
+      }
+      allClassNames.push(`ht${toUpperCaseFirst(key)}`);
+    });
+    removeClass(this.wtTable.wtRootElement.parentNode, allClassNames);
+    addClass(this.wtTable.wtRootElement.parentNode, newClassNames);
+  }
+
+  /**
+   * Get/Set Walkontable instance setting
+   *
+   * @param {String} key
+   * @param {*} [param1]
+   * @param {*} [param2]
+   * @param {*} [param3]
+   * @param {*} [param4]
+   * @returns {*}
+   */
+  getSetting(key, param1, param2, param3, param4) {
+    // this is faster than .apply - https://github.com/handsontable/handsontable/wiki/JavaScript-&-DOM-performance-tips
+    return this.wtSettings.getSetting(key, param1, param2, param3, param4);
+  }
+
+  /**
+   * Checks if setting exists
+   *
+   * @param {String} key
+   * @returns {Boolean}
+   */
+  hasSetting(key) {
+    return this.wtSettings.has(key);
+  }
+
+  /**
+   * Destroy instance
+   */
+  destroy() {
+    this.wtOverlays.destroy();
+    this.wtEvent.destroy();
+  }
+}
+
+export default Walkontable;

+ 342 - 0
handsontable/src/3rdparty/walkontable/src/event.js

@@ -0,0 +1,342 @@
+import {
+  closestDown,
+  hasClass,
+  isChildOf,
+  getParent,
+} from './../../../helpers/dom/element';
+import { partial } from './../../../helpers/function';
+import { isTouchSupported } from './../../../helpers/feature';
+import { isMobileBrowser } from './../../../helpers/browser';
+import EventManager from './../../../eventManager';
+
+const privatePool = new WeakMap();
+
+/**
+ * @class Event
+ */
+class Event {
+  /**
+   * @param {*} instance Walkontable instance.
+   */
+  constructor(instance) {
+    /**
+     * Instance of {@link Walkontable}.
+     *
+     * @private
+     * @type {Walkontable}
+     */
+    this.instance = instance;
+    /**
+     * Instance of {@link EventManager}.
+     *
+     * @private
+     * @type {EventManager}
+     */
+    this.eventManager = new EventManager(instance);
+
+    privatePool.set(this, {
+      selectedCellBeforeTouchEnd: void 0,
+      dblClickTimeout: [null, null],
+      dblClickOrigin: [null, null],
+    });
+
+    this.registerEvents();
+  }
+
+  /**
+   * Adds listeners for mouse and touch events.
+   *
+   * @private
+   */
+  registerEvents() {
+    this.eventManager.addEventListener(this.instance.wtTable.holder, 'contextmenu', event => this.onContextMenu(event));
+    this.eventManager.addEventListener(this.instance.wtTable.TABLE, 'mouseover', event => this.onMouseOver(event));
+    this.eventManager.addEventListener(this.instance.wtTable.TABLE, 'mouseout', event => this.onMouseOut(event));
+
+    const initTouchEvents = () => {
+      this.eventManager.addEventListener(this.instance.wtTable.holder, 'touchstart', event => this.onTouchStart(event));
+      this.eventManager.addEventListener(this.instance.wtTable.holder, 'touchend', event => this.onTouchEnd(event));
+
+      if (!this.instance.momentumScrolling) {
+        this.instance.momentumScrolling = {};
+      }
+      this.eventManager.addEventListener(this.instance.wtTable.holder, 'scroll', () => {
+        clearTimeout(this.instance.momentumScrolling._timeout);
+
+        if (!this.instance.momentumScrolling.ongoing) {
+          this.instance.getSetting('onBeforeTouchScroll');
+        }
+        this.instance.momentumScrolling.ongoing = true;
+
+        this.instance.momentumScrolling._timeout = setTimeout(() => {
+          if (!this.instance.touchApplied) {
+            this.instance.momentumScrolling.ongoing = false;
+
+            this.instance.getSetting('onAfterMomentumScroll');
+          }
+        }, 200);
+      });
+    };
+
+    const initMouseEvents = () => {
+      this.eventManager.addEventListener(this.instance.wtTable.holder, 'mouseup', event => this.onMouseUp(event));
+      this.eventManager.addEventListener(this.instance.wtTable.holder, 'mousedown', event => this.onMouseDown(event));
+    };
+
+    if (isMobileBrowser()) {
+      initTouchEvents();
+    } else {
+      // PC like devices which support both methods (touchscreen and ability to plug-in mouse).
+      if (isTouchSupported()) {
+        initTouchEvents();
+      }
+
+      initMouseEvents();
+    }
+
+    this.eventManager.addEventListener(window, 'resize', () => {
+      if (this.instance.getSetting('stretchH') !== 'none') {
+        this.instance.draw();
+      }
+    });
+  }
+
+  /**
+   * Checks if an element is already selected.
+   *
+   * @private
+   * @param {Element} touchTarget
+   * @returns {Boolean}
+   */
+  selectedCellWasTouched(touchTarget) {
+    const priv = privatePool.get(this);
+    const cellUnderFinger = this.parentCell(touchTarget);
+    const coordsOfCellUnderFinger = cellUnderFinger.coords;
+
+    if (priv.selectedCellBeforeTouchEnd && coordsOfCellUnderFinger) {
+      const [rowTouched, rowSelected] = [coordsOfCellUnderFinger.row, priv.selectedCellBeforeTouchEnd.from.row];
+      const [colTouched, colSelected] = [coordsOfCellUnderFinger.col, priv.selectedCellBeforeTouchEnd.from.col];
+
+      return rowTouched === rowSelected && colTouched === colSelected;
+    }
+
+    return false;
+  }
+
+  /**
+   * Gets closest TD or TH element.
+   *
+   * @private
+   * @param {Element} elem
+   * @returns {Object} Contains coordinates and reference to TD or TH if it exists. Otherwise it's empty object.
+   */
+  parentCell(elem) {
+    const cell = {};
+    const TABLE = this.instance.wtTable.TABLE;
+    const TD = closestDown(elem, ['TD', 'TH'], TABLE);
+
+    if (TD) {
+      cell.coords = this.instance.wtTable.getCoords(TD);
+      cell.TD = TD;
+
+    } else if (hasClass(elem, 'wtBorder') && hasClass(elem, 'current')) {
+      cell.coords = this.instance.selections.getCell().cellRange.highlight;
+      cell.TD = this.instance.wtTable.getCell(cell.coords);
+
+    } else if (hasClass(elem, 'wtBorder') && hasClass(elem, 'area')) {
+      if (this.instance.selections.createOrGetArea().cellRange) {
+        cell.coords = this.instance.selections.createOrGetArea().cellRange.to;
+        cell.TD = this.instance.wtTable.getCell(cell.coords);
+      }
+    }
+
+    return cell;
+  }
+
+  /**
+   * onMouseDown callback.
+   *
+   * @private
+   * @param {MouseEvent} event
+   */
+  onMouseDown(event) {
+    const priv = privatePool.get(this);
+    const activeElement = document.activeElement;
+    const getParentNode = partial(getParent, event.realTarget);
+    const realTarget = event.realTarget;
+
+    // ignore focusable element from mouse down processing (https://github.com/handsontable/handsontable/issues/3555)
+    if (realTarget === activeElement ||
+        getParentNode(0) === activeElement ||
+        getParentNode(1) === activeElement) {
+      return;
+    }
+
+    const cell = this.parentCell(realTarget);
+
+    if (hasClass(realTarget, 'corner')) {
+      this.instance.getSetting('onCellCornerMouseDown', event, realTarget);
+    } else if (cell.TD && this.instance.hasSetting('onCellMouseDown')) {
+      this.instance.getSetting('onCellMouseDown', event, cell.coords, cell.TD, this.instance);
+    }
+
+    if (event.button !== 2 && cell.TD) { // if not right mouse button
+      priv.dblClickOrigin[0] = cell.TD;
+
+      clearTimeout(priv.dblClickTimeout[0]);
+
+      priv.dblClickTimeout[0] = setTimeout(() => {
+        priv.dblClickOrigin[0] = null;
+      }, 1000);
+    }
+  }
+
+  /**
+   * onContextMenu callback.
+   *
+   * @private
+   * @param {MouseEvent} event
+   */
+  onContextMenu(event) {
+    if (this.instance.hasSetting('onCellContextMenu')) {
+      const cell = this.parentCell(event.realTarget);
+
+      if (cell.TD) {
+        this.instance.getSetting('onCellContextMenu', event, cell.coords, cell.TD, this.instance);
+      }
+    }
+  }
+
+  /**
+   * onMouseOver callback.
+   *
+   * @private
+   * @param {MouseEvent} event
+   */
+  onMouseOver(event) {
+    if (!this.instance.hasSetting('onCellMouseOver')) {
+      return;
+    }
+
+    const table = this.instance.wtTable.TABLE;
+    const td = closestDown(event.realTarget, ['TD', 'TH'], table);
+    const mainWOT = this.instance.cloneSource || this.instance;
+
+    if (td && td !== mainWOT.lastMouseOver && isChildOf(td, table)) {
+      mainWOT.lastMouseOver = td;
+
+      this.instance.getSetting('onCellMouseOver', event, this.instance.wtTable.getCoords(td), td, this.instance);
+    }
+  }
+
+  /**
+   * onMouseOut callback.
+   *
+   * @private
+   * @param {MouseEvent} event
+   */
+  onMouseOut(event) {
+    if (!this.instance.hasSetting('onCellMouseOut')) {
+      return;
+    }
+
+    const table = this.instance.wtTable.TABLE;
+    const lastTD = closestDown(event.realTarget, ['TD', 'TH'], table);
+    const nextTD = closestDown(event.relatedTarget, ['TD', 'TH'], table);
+
+    if (lastTD && lastTD !== nextTD && isChildOf(lastTD, table)) {
+      this.instance.getSetting('onCellMouseOut', event, this.instance.wtTable.getCoords(lastTD), lastTD, this.instance);
+    }
+  }
+
+  /**
+   * onMouseUp callback.
+   *
+   * @private
+   * @param {MouseEvent} event
+   */
+  onMouseUp(event) {
+    if (event.button === 2) {
+      return;
+    }
+
+    // if not right mouse button
+    const priv = privatePool.get(this);
+    const cell = this.parentCell(event.realTarget);
+
+    if (cell.TD && this.instance.hasSetting('onCellMouseUp')) {
+      this.instance.getSetting('onCellMouseUp', event, cell.coords, cell.TD, this.instance);
+    }
+
+    if (cell.TD === priv.dblClickOrigin[0] && cell.TD === priv.dblClickOrigin[1]) {
+      if (hasClass(event.realTarget, 'corner')) {
+        this.instance.getSetting('onCellCornerDblClick', event, cell.coords, cell.TD, this.instance);
+      } else {
+        this.instance.getSetting('onCellDblClick', event, cell.coords, cell.TD, this.instance);
+      }
+
+      priv.dblClickOrigin[0] = null;
+      priv.dblClickOrigin[1] = null;
+
+    } else if (cell.TD === priv.dblClickOrigin[0]) {
+      priv.dblClickOrigin[1] = cell.TD;
+
+      clearTimeout(priv.dblClickTimeout[1]);
+
+      priv.dblClickTimeout[1] = setTimeout(() => {
+        priv.dblClickOrigin[1] = null;
+      }, 500);
+    }
+  }
+
+  /**
+   * onTouchStart callback. Simulates mousedown event.
+   *
+   * @private
+   * @param {MouseEvent} event
+   */
+  onTouchStart(event) {
+    const priv = privatePool.get(this);
+
+    priv.selectedCellBeforeTouchEnd = this.instance.selections.getCell().cellRange;
+    this.instance.touchApplied = true;
+
+    this.onMouseDown(event);
+  }
+
+  /**
+   * onTouchEnd callback. Simulates mouseup event.
+   *
+   * @private
+   * @param {MouseEvent} event
+   */
+  onTouchEnd(event) {
+    const excludeTags = ['A', 'BUTTON', 'INPUT'];
+    const target = event.target;
+
+    this.instance.touchApplied = false;
+
+    // When the standard event was performed on the link element (a cell which contains HTML `a` element) then here
+    // we check if it should be canceled. Click is blocked in a situation when the element is rendered outside
+    // selected cells. This prevents accidentally page reloads while selecting adjacent cells.
+    if (this.selectedCellWasTouched(target) === false && excludeTags.includes(target.tagName)) {
+      event.preventDefault();
+    }
+
+    this.onMouseUp(event);
+  }
+
+  /**
+   * Clears double-click timeouts and destroys the internal eventManager instance.
+   */
+  destroy() {
+    const priv = privatePool.get(this);
+
+    clearTimeout(priv.dblClickTimeout[0]);
+    clearTimeout(priv.dblClickTimeout[1]);
+
+    this.eventManager.destroy();
+  }
+}
+
+export default Event;

+ 81 - 0
handsontable/src/3rdparty/walkontable/src/filter/column.js

@@ -0,0 +1,81 @@
+/**
+ * @class ColumnFilter
+ */
+class ColumnFilter {
+  /**
+   * @param {Number} offset
+   * @param {Number} total
+   * @param {Number} countTH
+   */
+  constructor(offset, total, countTH) {
+    this.offset = offset;
+    this.total = total;
+    this.countTH = countTH;
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  offsetted(index) {
+    return index + this.offset;
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  unOffsetted(index) {
+    return index - this.offset;
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  renderedToSource(index) {
+    return this.offsetted(index);
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  sourceToRendered(index) {
+    return this.unOffsetted(index);
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  offsettedTH(index) {
+    return index - this.countTH;
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  unOffsettedTH(index) {
+    return index + this.countTH;
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  visibleRowHeadedColumnToSourceColumn(index) {
+    return this.renderedToSource(this.offsettedTH(index));
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  sourceColumnToVisibleRowHeadedColumn(index) {
+    return this.unOffsettedTH(this.sourceToRendered(index));
+  }
+}
+
+export default ColumnFilter;

+ 81 - 0
handsontable/src/3rdparty/walkontable/src/filter/row.js

@@ -0,0 +1,81 @@
+/**
+ * @class RowFilter
+ */
+class RowFilter {
+  /**
+   * @param {Number} offset
+   * @param {Number} total
+   * @param {Number} countTH
+   */
+  constructor(offset, total, countTH) {
+    this.offset = offset;
+    this.total = total;
+    this.countTH = countTH;
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  offsetted(index) {
+    return index + this.offset;
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  unOffsetted(index) {
+    return index - this.offset;
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  renderedToSource(index) {
+    return this.offsetted(index);
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  sourceToRendered(index) {
+    return this.unOffsetted(index);
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  offsettedTH(index) {
+    return index - this.countTH;
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  unOffsettedTH(index) {
+    return index + this.countTH;
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  visibleColHeadedRowToSourceRow(index) {
+    return this.renderedToSource(this.offsettedTH(index));
+  }
+
+  /**
+   * @param index
+   * @returns {Number}
+   */
+  sourceRowToVisibleColHeadedRow(index) {
+    return this.unOffsettedTH(this.sourceToRendered(index));
+  }
+}
+
+export default RowFilter;

+ 58 - 0
handsontable/src/3rdparty/walkontable/src/index.js

@@ -0,0 +1,58 @@
+import '@babel/polyfill/lib/noConflict';
+
+import ViewportColumnsCalculator from './calculator/viewportColumns';
+import ViewportRowsCalculator from './calculator/viewportRows';
+
+import CellCoords from './cell/coords';
+import CellRange from './cell/range';
+
+import ColumnFilter from './filter/column';
+import RowFilter from './filter/row';
+
+import DebugOverlay from './overlay/debug';
+import LeftOverlay from './overlay/left';
+import TopOverlay from './overlay/top';
+import TopLeftCornerOverlay from './overlay/topLeftCorner';
+import BottomOverlay from './overlay/bottom';
+import BottomLeftCornerOverlay from './overlay/bottomLeftCorner';
+
+import Border from './border';
+import Walkontable from './core';
+import Event from './event';
+import Overlays from './overlays';
+import Scroll from './scroll';
+import Selection from './selection';
+import Settings from './settings';
+import Table from './table';
+import TableRenderer from './tableRenderer';
+import Viewport from './viewport';
+
+export {
+  ViewportColumnsCalculator,
+  ViewportRowsCalculator,
+
+  CellCoords,
+  CellRange,
+
+  ColumnFilter,
+  RowFilter,
+
+  DebugOverlay,
+  LeftOverlay,
+  TopOverlay,
+  TopLeftCornerOverlay,
+  BottomOverlay,
+  BottomLeftCornerOverlay,
+
+  Border,
+  Walkontable as default,
+  Walkontable as Core,
+  Event,
+  Overlays,
+  Scroll,
+  Selection,
+  Settings,
+  Table,
+  TableRenderer,
+  Viewport,
+};

+ 276 - 0
handsontable/src/3rdparty/walkontable/src/overlay/_base.js

@@ -0,0 +1,276 @@
+import {
+  getScrollableElement,
+  getTrimmingContainer,
+} from '../../../../helpers/dom/element';
+import { defineGetter } from '../../../../helpers/object';
+import { arrayEach } from '../../../../helpers/array';
+import EventManager from './../../../../eventManager';
+import Walkontable from './../core';
+
+const registeredOverlays = {};
+
+/**
+ * Creates an overlay over the original Walkontable instance. The overlay renders the clone of the original Walkontable
+ * and (optionally) implements behavior needed for native horizontal and vertical scrolling.
+ *
+ * @class Overlay
+ */
+class Overlay {
+  /**
+   * @type {String}
+   */
+  static get CLONE_TOP() {
+    return 'top';
+  }
+
+  /**
+   * @type {String}
+   */
+  static get CLONE_BOTTOM() {
+    return 'bottom';
+  }
+
+  /**
+   * @type {String}
+   */
+  static get CLONE_LEFT() {
+    return 'left';
+  }
+
+  /**
+   * @type {String}
+   */
+  static get CLONE_TOP_LEFT_CORNER() {
+    return 'top_left_corner';
+  }
+
+  /**
+   * @type {String}
+   */
+  static get CLONE_BOTTOM_LEFT_CORNER() {
+    return 'bottom_left_corner';
+  }
+
+  /**
+   * @type {String}
+   */
+  static get CLONE_DEBUG() {
+    return 'debug';
+  }
+
+  /**
+   * List of all availables clone types
+   *
+   * @type {Array}
+   */
+  static get CLONE_TYPES() {
+    return [
+      Overlay.CLONE_TOP,
+      Overlay.CLONE_BOTTOM,
+      Overlay.CLONE_LEFT,
+      Overlay.CLONE_TOP_LEFT_CORNER,
+      Overlay.CLONE_BOTTOM_LEFT_CORNER,
+      Overlay.CLONE_DEBUG,
+    ];
+  }
+
+  /**
+   * Register overlay class.
+   *
+   * @param {String} type Overlay type, one of the CLONE_TYPES value
+   * @param {Overlay} overlayClass Overlay class extended from base overlay class {@link Overlay}
+   */
+  static registerOverlay(type, overlayClass) {
+    if (Overlay.CLONE_TYPES.indexOf(type) === -1) {
+      throw new Error(`Unsupported overlay (${type}).`);
+    }
+    registeredOverlays[type] = overlayClass;
+  }
+
+  /**
+   * Create new instance of overlay type.
+   *
+   * @param {String} type Overlay type, one of the CLONE_TYPES value
+   * @param {Walkontable} wot Walkontable instance
+   */
+  static createOverlay(type, wot) {
+    return new registeredOverlays[type](wot);
+  }
+
+  /**
+   * Check if specified overlay was registered.
+   *
+   * @param {String} type Overlay type, one of the CLONE_TYPES value
+   * @returns {Boolean}
+   */
+  static hasOverlay(type) {
+    return registeredOverlays[type] !== void 0;
+  }
+
+  /**
+   * Checks if overlay object (`overlay`) is instance of overlay type (`type`).
+   *
+   * @param {Overlay} overlay Overlay object
+   * @param {String} type Overlay type, one of the CLONE_TYPES value
+   * @returns {Boolean}
+   */
+  static isOverlayTypeOf(overlay, type) {
+    if (!overlay || !registeredOverlays[type]) {
+      return false;
+    }
+
+    return overlay instanceof registeredOverlays[type];
+  }
+
+  /**
+   * @param {Walkontable} wotInstance
+   */
+  constructor(wotInstance) {
+    defineGetter(this, 'wot', wotInstance, {
+      writable: false,
+    });
+
+    // legacy support, deprecated in the future
+    this.instance = this.wot;
+
+    this.type = '';
+    this.mainTableScrollableElement = null;
+    this.TABLE = this.wot.wtTable.TABLE;
+    this.hider = this.wot.wtTable.hider;
+    this.spreader = this.wot.wtTable.spreader;
+    this.holder = this.wot.wtTable.holder;
+    this.wtRootElement = this.wot.wtTable.wtRootElement;
+    this.trimmingContainer = getTrimmingContainer(this.hider.parentNode.parentNode);
+    this.areElementSizesAdjusted = false;
+    this.updateStateOfRendering();
+  }
+
+  /**
+   * Update internal state of object with an information about the need of full rendering of the overlay.
+   *
+   * @returns {Boolean} Returns `true` if the state has changed since the last check.
+   */
+  updateStateOfRendering() {
+    const previousState = this.needFullRender;
+
+    this.needFullRender = this.shouldBeRendered();
+
+    const changed = previousState !== this.needFullRender;
+
+    if (changed && !this.needFullRender) {
+      this.reset();
+    }
+
+    return changed;
+  }
+
+  /**
+   * Checks if overlay should be fully rendered
+   *
+   * @returns {Boolean}
+   */
+  shouldBeRendered() {
+    return true;
+  }
+
+  /**
+   * Update the trimming container.
+   */
+  updateTrimmingContainer() {
+    this.trimmingContainer = getTrimmingContainer(this.hider.parentNode.parentNode);
+  }
+
+  /**
+   * Update the main scrollable element.
+   */
+  updateMainScrollableElement() {
+    this.mainTableScrollableElement = getScrollableElement(this.wot.wtTable.TABLE);
+  }
+
+  /**
+   * Make a clone of table for overlay
+   *
+   * @param {String} direction Can be `Overlay.CLONE_TOP`, `Overlay.CLONE_LEFT`,
+   *                           `Overlay.CLONE_TOP_LEFT_CORNER`, `Overlay.CLONE_DEBUG`
+   * @returns {Walkontable}
+   */
+  makeClone(direction) {
+    if (Overlay.CLONE_TYPES.indexOf(direction) === -1) {
+      throw new Error(`Clone type "${direction}" is not supported.`);
+    }
+    const clone = document.createElement('DIV');
+    const clonedTable = document.createElement('TABLE');
+
+    clone.className = `ht_clone_${direction} handsontable`;
+    clone.style.position = 'absolute';
+    clone.style.top = 0;
+    clone.style.left = 0;
+    clone.style.overflow = 'hidden';
+
+    clonedTable.className = this.wot.wtTable.TABLE.className;
+    clone.appendChild(clonedTable);
+
+    this.type = direction;
+    this.wot.wtTable.wtRootElement.parentNode.appendChild(clone);
+
+    const preventOverflow = this.wot.getSetting('preventOverflow');
+
+    if (preventOverflow === true ||
+        preventOverflow === 'horizontal' && this.type === Overlay.CLONE_TOP ||
+        preventOverflow === 'vertical' && this.type === Overlay.CLONE_LEFT) {
+      this.mainTableScrollableElement = window;
+
+    } else {
+      this.mainTableScrollableElement = getScrollableElement(this.wot.wtTable.TABLE);
+    }
+
+    return new Walkontable({
+      cloneSource: this.wot,
+      cloneOverlay: this,
+      table: clonedTable,
+    });
+  }
+
+  /**
+   * Refresh/Redraw overlay
+   *
+   * @param {Boolean} [fastDraw=false]
+   */
+  refresh(fastDraw = false) {
+    // When hot settings are changed we allow to refresh overlay once before blocking
+    const nextCycleRenderFlag = this.shouldBeRendered();
+
+    if (this.clone && (this.needFullRender || nextCycleRenderFlag)) {
+      this.clone.draw(fastDraw);
+    }
+    this.needFullRender = nextCycleRenderFlag;
+  }
+
+  /**
+   * Reset overlay styles to initial values.
+   */
+  reset() {
+    if (!this.clone) {
+      return;
+    }
+    const holder = this.clone.wtTable.holder;
+    const hider = this.clone.wtTable.hider;
+    const holderStyle = holder.style;
+    const hidderStyle = hider.style;
+    const rootStyle = holder.parentNode.style;
+
+    arrayEach([holderStyle, hidderStyle, rootStyle], (style) => {
+      style.width = '';
+      style.height = '';
+    });
+  }
+
+  /**
+   * Destroy overlay instance
+   */
+  destroy() {
+    (new EventManager(this.clone)).destroy();
+  }
+}
+
+export default Overlay;

+ 313 - 0
handsontable/src/3rdparty/walkontable/src/overlay/bottom.js

@@ -0,0 +1,313 @@
+import {
+  addClass,
+  getScrollbarWidth,
+  getScrollTop,
+  getWindowScrollLeft,
+  hasClass,
+  outerHeight,
+  removeClass,
+  resetCssTransform
+} from './../../../../helpers/dom/element';
+import Overlay from './_base';
+
+/**
+ * @class BottomOverlay
+ */
+class BottomOverlay extends Overlay {
+  /**
+   * @param {Walkontable} wotInstance
+   */
+  constructor(wotInstance) {
+    super(wotInstance);
+    this.clone = this.makeClone(Overlay.CLONE_BOTTOM);
+  }
+
+  /**
+   *
+   */
+  repositionOverlay() {
+    let scrollbarWidth = getScrollbarWidth();
+    const cloneRoot = this.clone.wtTable.holder.parentNode;
+
+    if (this.wot.wtTable.holder.clientHeight === this.wot.wtTable.holder.offsetHeight) {
+      scrollbarWidth = 0;
+    }
+
+    cloneRoot.style.top = '';
+    cloneRoot.style.bottom = `${scrollbarWidth}px`;
+  }
+
+  /**
+   * Checks if overlay should be fully rendered
+   *
+   * @returns {Boolean}
+   */
+  shouldBeRendered() {
+    /* eslint-disable no-unneeded-ternary */
+    return this.wot.getSetting('fixedRowsBottom') ? true : false;
+  }
+
+  /**
+   * Updates the top overlay position
+   */
+  resetFixedPosition() {
+    if (!this.needFullRender || !this.wot.wtTable.holder.parentNode) {
+      // removed from DOM
+      return;
+    }
+
+    const overlayRoot = this.clone.wtTable.holder.parentNode;
+    let headerPosition = 0;
+    overlayRoot.style.top = '';
+
+    if (this.wot.wtOverlays.leftOverlay.trimmingContainer === window) {
+      const box = this.wot.wtTable.hider.getBoundingClientRect();
+      const bottom = Math.ceil(box.bottom);
+      let finalLeft;
+      let finalBottom;
+      const bodyHeight = document.body.offsetHeight;
+
+      finalLeft = this.wot.wtTable.hider.style.left;
+      finalLeft = finalLeft === '' ? 0 : finalLeft;
+
+      if (bottom > bodyHeight) {
+        finalBottom = (bottom - bodyHeight);
+      } else {
+        finalBottom = 0;
+      }
+      headerPosition = finalBottom;
+      finalBottom += 'px';
+
+      overlayRoot.style.top = '';
+      overlayRoot.style.left = finalLeft;
+      overlayRoot.style.bottom = finalBottom;
+
+    } else {
+      headerPosition = this.getScrollPosition();
+      resetCssTransform(overlayRoot);
+      this.repositionOverlay();
+    }
+    this.adjustHeaderBordersPosition(headerPosition);
+  }
+
+  /**
+   * Sets the main overlay's vertical scroll position
+   *
+   * @param {Number} pos
+   */
+  setScrollPosition(pos) {
+    if (this.mainTableScrollableElement === window) {
+      window.scrollTo(getWindowScrollLeft(), pos);
+
+    } else {
+      this.mainTableScrollableElement.scrollTop = pos;
+    }
+  }
+
+  /**
+   * Triggers onScroll hook callback
+   */
+  onScroll() {
+    this.wot.getSetting('onScrollVertically');
+  }
+
+  /**
+   * Calculates total sum cells height
+   *
+   * @param {Number} from Row index which calculates started from
+   * @param {Number} to Row index where calculation is finished
+   * @returns {Number} Height sum
+   */
+  sumCellSizes(from, to) {
+    let row = from;
+    let sum = 0;
+    const defaultRowHeight = this.wot.wtSettings.settings.defaultRowHeight;
+
+    while (row < to) {
+      const height = this.wot.wtTable.getRowHeight(row);
+
+      sum += height === void 0 ? defaultRowHeight : height;
+      row += 1;
+    }
+
+    return sum;
+  }
+
+  /**
+   * Adjust overlay root element, childs and master table element sizes (width, height).
+   *
+   * @param {Boolean} [force=false]
+   */
+  adjustElementsSize(force = false) {
+    this.updateTrimmingContainer();
+
+    if (this.needFullRender || force) {
+      this.adjustRootElementSize();
+      this.adjustRootChildrenSize();
+
+      if (!force) {
+        this.areElementSizesAdjusted = true;
+      }
+    }
+  }
+
+  /**
+   * Adjust overlay root element size (width and height).
+   */
+  adjustRootElementSize() {
+    const masterHolder = this.wot.wtTable.holder;
+    const scrollbarWidth = masterHolder.clientWidth === masterHolder.offsetWidth ? 0 : getScrollbarWidth();
+    const overlayRoot = this.clone.wtTable.holder.parentNode;
+    const overlayRootStyle = overlayRoot.style;
+
+    if (this.trimmingContainer === window) {
+      overlayRootStyle.width = '';
+
+    } else {
+      overlayRootStyle.width = `${this.wot.wtViewport.getWorkspaceWidth() - scrollbarWidth}px`;
+    }
+
+    this.clone.wtTable.holder.style.width = overlayRootStyle.width;
+
+    const tableHeight = outerHeight(this.clone.wtTable.TABLE);
+    overlayRootStyle.height = `${tableHeight === 0 ? tableHeight : tableHeight}px`;
+  }
+
+  /**
+   * Adjust overlay root childs size
+   */
+  adjustRootChildrenSize() {
+    let scrollbarWidth = getScrollbarWidth();
+
+    this.clone.wtTable.hider.style.width = this.hider.style.width;
+    this.clone.wtTable.holder.style.width = this.clone.wtTable.holder.parentNode.style.width;
+
+    if (scrollbarWidth === 0) {
+      scrollbarWidth = 30;
+    }
+    this.clone.wtTable.holder.style.height = `${parseInt(this.clone.wtTable.holder.parentNode.style.height, 10) + scrollbarWidth}px`;
+  }
+
+  /**
+   * Adjust the overlay dimensions and position
+   */
+  applyToDOM() {
+    const total = this.wot.getSetting('totalRows');
+
+    if (!this.areElementSizesAdjusted) {
+      this.adjustElementsSize();
+    }
+    if (typeof this.wot.wtViewport.rowsRenderCalculator.startPosition === 'number') {
+      this.spreader.style.top = `${this.wot.wtViewport.rowsRenderCalculator.startPosition}px`;
+
+    } else if (total === 0) {
+      // can happen if there are 0 rows
+      this.spreader.style.top = '0';
+
+    } else {
+      throw new Error('Incorrect value of the rowsRenderCalculator');
+    }
+    this.spreader.style.bottom = '';
+
+    if (this.needFullRender) {
+      this.syncOverlayOffset();
+    }
+  }
+
+  /**
+   * Synchronize calculated left position to an element
+   */
+  syncOverlayOffset() {
+    if (typeof this.wot.wtViewport.columnsRenderCalculator.startPosition === 'number') {
+      this.clone.wtTable.spreader.style.left = `${this.wot.wtViewport.columnsRenderCalculator.startPosition}px`;
+
+    } else {
+      this.clone.wtTable.spreader.style.left = '';
+    }
+  }
+
+  /**
+   * Scrolls vertically to a row
+   *
+   * @param sourceRow {Number} Row index which you want to scroll to
+   * @param [bottomEdge=false] {Boolean} if `true`, scrolls according to the bottom edge (top edge is by default)
+   */
+  scrollTo(sourceRow, bottomEdge) {
+    let newY = this.getTableParentOffset();
+    const sourceInstance = this.wot.cloneSource ? this.wot.cloneSource : this.wot;
+    const mainHolder = sourceInstance.wtTable.holder;
+    let scrollbarCompensation = 0;
+
+    if (bottomEdge && mainHolder.offsetHeight !== mainHolder.clientHeight) {
+      scrollbarCompensation = getScrollbarWidth();
+    }
+
+    if (bottomEdge) {
+      newY += this.sumCellSizes(0, sourceRow + 1);
+      newY -= this.wot.wtViewport.getViewportHeight();
+      // Fix 1 pixel offset when cell is selected
+      newY += 1;
+
+    } else {
+      newY += this.sumCellSizes(this.wot.getSetting('fixedRowsBottom'), sourceRow);
+    }
+    newY += scrollbarCompensation;
+
+    this.setScrollPosition(newY);
+  }
+
+  /**
+   * Gets table parent top position
+   *
+   * @returns {Number}
+   */
+  getTableParentOffset() {
+    if (this.mainTableScrollableElement === window) {
+      return this.wot.wtTable.holderOffset.top;
+    }
+
+    return 0;
+  }
+
+  /**
+   * Gets the main overlay's vertical scroll position
+   *
+   * @returns {Number} Main table's vertical scroll position
+   */
+  getScrollPosition() {
+    return getScrollTop(this.mainTableScrollableElement);
+  }
+
+  /**
+   * Adds css classes to hide the header border's header (cell-selection border hiding issue)
+   *
+   * @param {Number} position Header Y position if trimming container is window or scroll top if not
+   */
+  adjustHeaderBordersPosition(position) {
+    if (this.wot.getSetting('fixedRowsBottom') === 0 && this.wot.getSetting('columnHeaders').length > 0) {
+      const masterParent = this.wot.wtTable.holder.parentNode;
+      const previousState = hasClass(masterParent, 'innerBorderTop');
+
+      if (position) {
+        addClass(masterParent, 'innerBorderTop');
+      } else {
+        removeClass(masterParent, 'innerBorderTop');
+      }
+      if (!previousState && position || previousState && !position) {
+        this.wot.wtOverlays.adjustElementsSize();
+      }
+    }
+    // nasty workaround for double border in the header, TODO: find a pure-css solution
+    if (this.wot.getSetting('rowHeaders').length === 0) {
+      const secondHeaderCell = this.clone.wtTable.THEAD.querySelector('th:nth-of-type(2)');
+
+      if (secondHeaderCell) {
+        secondHeaderCell.style['border-left-width'] = 0;
+      }
+    }
+  }
+}
+
+Overlay.registerOverlay(Overlay.CLONE_BOTTOM, BottomOverlay);
+
+export default BottomOverlay;

+ 100 - 0
handsontable/src/3rdparty/walkontable/src/overlay/bottomLeftCorner.js

@@ -0,0 +1,100 @@
+import {
+  getScrollbarWidth,
+  outerHeight,
+  outerWidth,
+  resetCssTransform
+} from './../../../../helpers/dom/element';
+import Overlay from './_base';
+
+/**
+ * @class TopLeftCornerOverlay
+ */
+class BottomLeftCornerOverlay extends Overlay {
+  /**
+   * @param {Walkontable} wotInstance
+   */
+  constructor(wotInstance) {
+    super(wotInstance);
+    this.clone = this.makeClone(Overlay.CLONE_BOTTOM_LEFT_CORNER);
+  }
+
+  /**
+   * Checks if overlay should be fully rendered
+   *
+   * @returns {Boolean}
+   */
+  shouldBeRendered() {
+    /* eslint-disable no-unneeded-ternary */
+    return this.wot.getSetting('fixedRowsBottom') &&
+      (this.wot.getSetting('fixedColumnsLeft') || this.wot.getSetting('rowHeaders').length) ? true : false;
+  }
+
+  /**
+   * Reposition the overlay.
+   */
+  repositionOverlay() {
+    let scrollbarWidth = getScrollbarWidth();
+    const cloneRoot = this.clone.wtTable.holder.parentNode;
+
+    if (this.wot.wtTable.holder.clientHeight === this.wot.wtTable.holder.offsetHeight) {
+      scrollbarWidth = 0;
+    }
+
+    cloneRoot.style.top = '';
+    cloneRoot.style.bottom = `${scrollbarWidth}px`;
+  }
+
+  /**
+   * Updates the corner overlay position
+   */
+  resetFixedPosition() {
+    this.updateTrimmingContainer();
+
+    if (!this.wot.wtTable.holder.parentNode) {
+      // removed from DOM
+      return;
+    }
+    const overlayRoot = this.clone.wtTable.holder.parentNode;
+    const tableHeight = outerHeight(this.clone.wtTable.TABLE);
+    const tableWidth = outerWidth(this.clone.wtTable.TABLE);
+
+    overlayRoot.style.top = '';
+
+    if (this.trimmingContainer === window) {
+      const box = this.wot.wtTable.hider.getBoundingClientRect();
+      const bottom = Math.ceil(box.bottom);
+      const left = Math.ceil(box.left);
+      let finalLeft;
+      let finalBottom;
+      const bodyHeight = document.body.offsetHeight;
+
+      if (left < 0) {
+        finalLeft = -left;
+      } else {
+        finalLeft = 0;
+      }
+
+      if (bottom > bodyHeight) {
+        finalBottom = (bottom - bodyHeight);
+      } else {
+        finalBottom = 0;
+      }
+      finalBottom += 'px';
+      finalLeft += 'px';
+
+      overlayRoot.style.top = '';
+      overlayRoot.style.left = finalLeft;
+      overlayRoot.style.bottom = finalBottom;
+
+    } else {
+      resetCssTransform(overlayRoot);
+      this.repositionOverlay();
+    }
+    overlayRoot.style.height = `${tableHeight === 0 ? tableHeight : tableHeight}px`;
+    overlayRoot.style.width = `${tableWidth === 0 ? tableWidth : tableWidth}px`;
+  }
+}
+
+Overlay.registerOverlay(Overlay.CLONE_BOTTOM_LEFT_CORNER, BottomLeftCornerOverlay);
+
+export default BottomLeftCornerOverlay;

+ 27 - 0
handsontable/src/3rdparty/walkontable/src/overlay/debug.js

@@ -0,0 +1,27 @@
+import { addClass } from './../../../../helpers/dom/element';
+import Overlay from './_base';
+
+/**
+ * A overlay that renders ALL available rows & columns positioned on top of the original Walkontable instance and all other overlays.
+ * Used for debugging purposes to see if the other overlays (that render only part of the rows & columns) are positioned correctly
+ *
+ * @class DebugOverlay
+ */
+class DebugOverlay extends Overlay {
+  /**
+   * @param {Walkontable} wotInstance
+   */
+  constructor(wotInstance) {
+    super(wotInstance);
+
+    this.clone = this.makeClone(Overlay.CLONE_DEBUG);
+    this.clone.wtTable.holder.style.opacity = 0.4;
+    this.clone.wtTable.holder.style.textShadow = '0 0 2px #ff0000';
+
+    addClass(this.clone.wtTable.holder.parentNode, 'wtDebugVisible');
+  }
+}
+
+Overlay.registerOverlay(Overlay.CLONE_DEBUG, DebugOverlay);
+
+export default DebugOverlay;

+ 312 - 0
handsontable/src/3rdparty/walkontable/src/overlay/left.js

@@ -0,0 +1,312 @@
+import {
+  addClass,
+  getScrollbarWidth,
+  getScrollLeft,
+  getWindowScrollTop,
+  hasClass,
+  outerWidth,
+  innerHeight,
+  removeClass,
+  setOverlayPosition,
+  resetCssTransform
+} from './../../../../helpers/dom/element';
+import Overlay from './_base';
+
+/**
+ * @class LeftOverlay
+ */
+class LeftOverlay extends Overlay {
+  /**
+   * @param {Walkontable} wotInstance
+   */
+  constructor(wotInstance) {
+    super(wotInstance);
+    this.clone = this.makeClone(Overlay.CLONE_LEFT);
+  }
+
+  /**
+   * Checks if overlay should be fully rendered.
+   *
+   * @returns {Boolean}
+   */
+  shouldBeRendered() {
+    return !!(this.wot.getSetting('fixedColumnsLeft') || this.wot.getSetting('rowHeaders').length);
+  }
+
+  /**
+   * Updates the left overlay position.
+   */
+  resetFixedPosition() {
+    if (!this.needFullRender || !this.wot.wtTable.holder.parentNode) {
+      // removed from DOM
+      return;
+    }
+    const overlayRoot = this.clone.wtTable.holder.parentNode;
+    let headerPosition = 0;
+    const preventOverflow = this.wot.getSetting('preventOverflow');
+
+    if (this.trimmingContainer === window && (!preventOverflow || preventOverflow !== 'horizontal')) {
+      const box = this.wot.wtTable.hider.getBoundingClientRect();
+      const left = Math.ceil(box.left);
+      const right = Math.ceil(box.right);
+      let finalLeft;
+      let finalTop;
+
+      finalTop = this.wot.wtTable.hider.style.top;
+      finalTop = finalTop === '' ? 0 : finalTop;
+
+      if (left < 0 && (right - overlayRoot.offsetWidth) > 0) {
+        finalLeft = -left;
+      } else {
+        finalLeft = 0;
+      }
+      headerPosition = finalLeft;
+      finalLeft += 'px';
+
+      setOverlayPosition(overlayRoot, finalLeft, finalTop);
+
+    } else {
+      headerPosition = this.getScrollPosition();
+      resetCssTransform(overlayRoot);
+    }
+    this.adjustHeaderBordersPosition(headerPosition);
+    this.adjustElementsSize();
+  }
+
+  /**
+   * Sets the main overlay's horizontal scroll position.
+   *
+   * @param {Number} pos
+   * @returns {Boolean}
+   */
+  setScrollPosition(pos) {
+    let result = false;
+
+    if (this.mainTableScrollableElement === window && window.scrollX !== pos) {
+      window.scrollTo(pos, getWindowScrollTop());
+      result = true;
+
+    } else if (this.mainTableScrollableElement.scrollLeft !== pos) {
+      this.mainTableScrollableElement.scrollLeft = pos;
+      result = true;
+    }
+
+    return result;
+  }
+
+  /**
+   * Triggers onScroll hook callback.
+   */
+  onScroll() {
+    this.wot.getSetting('onScrollVertically');
+  }
+
+  /**
+   * Calculates total sum cells width.
+   *
+   * @param {Number} from Column index which calculates started from.
+   * @param {Number} to Column index where calculation is finished.
+   * @returns {Number} Width sum.
+   */
+  sumCellSizes(from, to) {
+    const defaultColumnWidth = this.wot.wtSettings.defaultColumnWidth;
+    let column = from;
+    let sum = 0;
+
+    while (column < to) {
+      sum += this.wot.wtTable.getStretchedColumnWidth(column) || defaultColumnWidth;
+      column += 1;
+    }
+
+    return sum;
+  }
+
+  /**
+   * Adjust overlay root element, childs and master table element sizes (width, height).
+   *
+   * @param {Boolean} [force=false]
+   */
+  adjustElementsSize(force = false) {
+    this.updateTrimmingContainer();
+
+    if (this.needFullRender || force) {
+      this.adjustRootElementSize();
+      this.adjustRootChildrenSize();
+
+      if (!force) {
+        this.areElementSizesAdjusted = true;
+      }
+    }
+  }
+
+  /**
+   * Adjust overlay root element size (width and height).
+   */
+  adjustRootElementSize() {
+    const masterHolder = this.wot.wtTable.holder;
+    const scrollbarHeight = masterHolder.clientHeight === masterHolder.offsetHeight ? 0 : getScrollbarWidth();
+    const overlayRoot = this.clone.wtTable.holder.parentNode;
+    const overlayRootStyle = overlayRoot.style;
+    const preventOverflow = this.wot.getSetting('preventOverflow');
+
+    if (this.trimmingContainer !== window || preventOverflow === 'vertical') {
+      let height = this.wot.wtViewport.getWorkspaceHeight() - scrollbarHeight;
+
+      height = Math.min(height, innerHeight(this.wot.wtTable.wtRootElement));
+
+      overlayRootStyle.height = `${height}px`;
+
+    } else {
+      overlayRootStyle.height = '';
+    }
+
+    this.clone.wtTable.holder.style.height = overlayRootStyle.height;
+
+    const tableWidth = outerWidth(this.clone.wtTable.TABLE);
+    overlayRootStyle.width = `${tableWidth === 0 ? tableWidth : tableWidth + 4}px`;
+  }
+
+  /**
+   * Adjust overlay root childs size.
+   */
+  adjustRootChildrenSize() {
+    let scrollbarWidth = getScrollbarWidth();
+
+    this.clone.wtTable.hider.style.height = this.hider.style.height;
+    this.clone.wtTable.holder.style.height = this.clone.wtTable.holder.parentNode.style.height;
+
+    if (scrollbarWidth === 0) {
+      scrollbarWidth = 30;
+    }
+    this.clone.wtTable.holder.style.width = `${parseInt(this.clone.wtTable.holder.parentNode.style.width, 10) + scrollbarWidth}px`;
+  }
+
+  /**
+   * Adjust the overlay dimensions and position.
+   */
+  applyToDOM() {
+    const total = this.wot.getSetting('totalColumns');
+
+    if (!this.areElementSizesAdjusted) {
+      this.adjustElementsSize();
+    }
+    if (typeof this.wot.wtViewport.columnsRenderCalculator.startPosition === 'number') {
+      this.spreader.style.left = `${this.wot.wtViewport.columnsRenderCalculator.startPosition}px`;
+
+    } else if (total === 0) {
+      this.spreader.style.left = '0';
+
+    } else {
+      throw new Error('Incorrect value of the columnsRenderCalculator');
+    }
+    this.spreader.style.right = '';
+
+    if (this.needFullRender) {
+      this.syncOverlayOffset();
+    }
+  }
+
+  /**
+   * Synchronize calculated top position to an element.
+   */
+  syncOverlayOffset() {
+    if (typeof this.wot.wtViewport.rowsRenderCalculator.startPosition === 'number') {
+      this.clone.wtTable.spreader.style.top = `${this.wot.wtViewport.rowsRenderCalculator.startPosition}px`;
+
+    } else {
+      this.clone.wtTable.spreader.style.top = '';
+    }
+  }
+
+  /**
+   * Scrolls horizontally to a column at the left edge of the viewport.
+   *
+   * @param {Number} sourceCol  Column index which you want to scroll to.
+   * @param {Boolean} [beyondRendered]  if `true`, scrolls according to the bottom edge (top edge is by default).
+   * @returns {Boolean}
+   */
+  scrollTo(sourceCol, beyondRendered) {
+    let newX = this.getTableParentOffset();
+    const sourceInstance = this.wot.cloneSource ? this.wot.cloneSource : this.wot;
+    const mainHolder = sourceInstance.wtTable.holder;
+    let scrollbarCompensation = 0;
+
+    if (beyondRendered && mainHolder.offsetWidth !== mainHolder.clientWidth) {
+      scrollbarCompensation = getScrollbarWidth();
+    }
+    if (beyondRendered) {
+      newX += this.sumCellSizes(0, sourceCol + 1);
+      newX -= this.wot.wtViewport.getViewportWidth();
+
+    } else {
+      newX += this.sumCellSizes(this.wot.getSetting('fixedColumnsLeft'), sourceCol);
+    }
+
+    newX += scrollbarCompensation;
+
+    return this.setScrollPosition(newX);
+  }
+
+  /**
+   * Gets table parent left position.
+   *
+   * @returns {Number}
+   */
+  getTableParentOffset() {
+    const preventOverflow = this.wot.getSetting('preventOverflow');
+    let offset = 0;
+
+    if (!preventOverflow && this.trimmingContainer === window) {
+      offset = this.wot.wtTable.holderOffset.left;
+    }
+
+    return offset;
+  }
+
+  /**
+   * Gets the main overlay's horizontal scroll position.
+   *
+   * @returns {Number} Main table's vertical scroll position.
+   */
+  getScrollPosition() {
+    return getScrollLeft(this.mainTableScrollableElement);
+  }
+
+  /**
+   * Adds css classes to hide the header border's header (cell-selection border hiding issue).
+   *
+   * @param {Number} position Header X position if trimming container is window or scroll top if not.
+   */
+  adjustHeaderBordersPosition(position) {
+    const masterParent = this.wot.wtTable.holder.parentNode;
+    const rowHeaders = this.wot.getSetting('rowHeaders');
+    const fixedColumnsLeft = this.wot.getSetting('fixedColumnsLeft');
+    const totalRows = this.wot.getSetting('totalRows');
+
+    if (totalRows) {
+      removeClass(masterParent, 'emptyRows');
+    } else {
+      addClass(masterParent, 'emptyRows');
+    }
+
+    if (fixedColumnsLeft && !rowHeaders.length) {
+      addClass(masterParent, 'innerBorderLeft');
+
+    } else if (!fixedColumnsLeft && rowHeaders.length) {
+      const previousState = hasClass(masterParent, 'innerBorderLeft');
+
+      if (position) {
+        addClass(masterParent, 'innerBorderLeft');
+      } else {
+        removeClass(masterParent, 'innerBorderLeft');
+      }
+      if (!previousState && position || previousState && !position) {
+        this.wot.wtOverlays.adjustElementsSize();
+      }
+    }
+  }
+}
+
+Overlay.registerOverlay(Overlay.CLONE_LEFT, LeftOverlay);
+
+export default LeftOverlay;

+ 362 - 0
handsontable/src/3rdparty/walkontable/src/overlay/top.js

@@ -0,0 +1,362 @@
+import {
+  addClass,
+  getScrollbarWidth,
+  getScrollTop,
+  getWindowScrollLeft,
+  hasClass,
+  outerHeight,
+  innerWidth,
+  removeClass,
+  setOverlayPosition,
+  resetCssTransform
+} from './../../../../helpers/dom/element';
+import { arrayEach } from './../../../../helpers/array';
+import Overlay from './_base';
+
+/**
+ * @class TopOverlay
+ */
+class TopOverlay extends Overlay {
+  /**
+   * @param {Walkontable} wotInstance
+   */
+  constructor(wotInstance) {
+    super(wotInstance);
+    this.clone = this.makeClone(Overlay.CLONE_TOP);
+  }
+
+  /**
+   * Checks if overlay should be fully rendered.
+   *
+   * @returns {Boolean}
+   */
+  shouldBeRendered() {
+    return !!(this.wot.getSetting('fixedRowsTop') || this.wot.getSetting('columnHeaders').length);
+  }
+
+  /**
+   * Updates the top overlay position.
+   */
+  resetFixedPosition() {
+    if (!this.needFullRender || !this.wot.wtTable.holder.parentNode) {
+      // removed from DOM
+      return;
+    }
+    const overlayRoot = this.clone.wtTable.holder.parentNode;
+    let headerPosition = 0;
+    const preventOverflow = this.wot.getSetting('preventOverflow');
+
+    if (this.trimmingContainer === window && (!preventOverflow || preventOverflow !== 'vertical')) {
+      const box = this.wot.wtTable.hider.getBoundingClientRect();
+      const top = Math.ceil(box.top);
+      const bottom = Math.ceil(box.bottom);
+      let finalLeft;
+      let finalTop;
+
+      finalLeft = this.wot.wtTable.hider.style.left;
+      finalLeft = finalLeft === '' ? 0 : finalLeft;
+
+      if (top < 0 && (bottom - overlayRoot.offsetHeight) > 0) {
+        finalTop = -top;
+      } else {
+        finalTop = 0;
+      }
+      headerPosition = finalTop;
+      finalTop += 'px';
+
+      setOverlayPosition(overlayRoot, finalLeft, finalTop);
+
+    } else {
+      headerPosition = this.getScrollPosition();
+      resetCssTransform(overlayRoot);
+    }
+
+    this.adjustHeaderBordersPosition(headerPosition);
+    this.adjustElementsSize();
+  }
+
+  /**
+   * Sets the main overlay's vertical scroll position.
+   *
+   * @param {Number} pos
+   * @returns {Boolean}
+   */
+  setScrollPosition(pos) {
+    let result = false;
+
+    if (this.mainTableScrollableElement === window && window.scrollY !== pos) {
+      window.scrollTo(getWindowScrollLeft(), pos);
+      result = true;
+
+    } else if (this.mainTableScrollableElement.scrollTop !== pos) {
+      this.mainTableScrollableElement.scrollTop = pos;
+      result = true;
+    }
+
+    return result;
+  }
+
+  /**
+   * Triggers onScroll hook callback.
+   */
+  onScroll() {
+    this.wot.getSetting('onScrollHorizontally');
+  }
+
+  /**
+   * Calculates total sum cells height.
+   *
+   * @param {Number} from Row index which calculates started from.
+   * @param {Number} to Row index where calculation is finished.
+   * @returns {Number} Height sum.
+   */
+  sumCellSizes(from, to) {
+    const defaultRowHeight = this.wot.wtSettings.settings.defaultRowHeight;
+    let row = from;
+    let sum = 0;
+
+    while (row < to) {
+      const height = this.wot.wtTable.getRowHeight(row);
+
+      sum += height === void 0 ? defaultRowHeight : height;
+      row += 1;
+    }
+
+    return sum;
+  }
+
+  /**
+   * Adjust overlay root element, childs and master table element sizes (width, height).
+   *
+   * @param {Boolean} [force=false]
+   */
+  adjustElementsSize(force = false) {
+    this.updateTrimmingContainer();
+
+    if (this.needFullRender || force) {
+      this.adjustRootElementSize();
+      this.adjustRootChildrenSize();
+
+      if (!force) {
+        this.areElementSizesAdjusted = true;
+      }
+    }
+  }
+
+  /**
+   * Adjust overlay root element size (width and height).
+   */
+  adjustRootElementSize() {
+    const masterHolder = this.wot.wtTable.holder;
+    const scrollbarWidth = masterHolder.clientWidth === masterHolder.offsetWidth ? 0 : getScrollbarWidth();
+    const overlayRoot = this.clone.wtTable.holder.parentNode;
+    const overlayRootStyle = overlayRoot.style;
+    const preventOverflow = this.wot.getSetting('preventOverflow');
+
+    if (this.trimmingContainer !== window || preventOverflow === 'horizontal') {
+      let width = this.wot.wtViewport.getWorkspaceWidth() - scrollbarWidth;
+
+      width = Math.min(width, innerWidth(this.wot.wtTable.wtRootElement));
+
+      overlayRootStyle.width = `${width}px`;
+
+    } else {
+      overlayRootStyle.width = '';
+    }
+
+    this.clone.wtTable.holder.style.width = overlayRootStyle.width;
+
+    const tableHeight = outerHeight(this.clone.wtTable.TABLE);
+    overlayRootStyle.height = `${tableHeight === 0 ? tableHeight : tableHeight + 4}px`;
+  }
+
+  /**
+   * Adjust overlay root childs size.
+   */
+  adjustRootChildrenSize() {
+    let scrollbarWidth = getScrollbarWidth();
+
+    this.clone.wtTable.hider.style.width = this.hider.style.width;
+    this.clone.wtTable.holder.style.width = this.clone.wtTable.holder.parentNode.style.width;
+
+    if (scrollbarWidth === 0) {
+      scrollbarWidth = 30;
+    }
+    this.clone.wtTable.holder.style.height = `${parseInt(this.clone.wtTable.holder.parentNode.style.height, 10) + scrollbarWidth}px`;
+  }
+
+  /**
+   * Adjust the overlay dimensions and position.
+   */
+  applyToDOM() {
+    const total = this.wot.getSetting('totalRows');
+
+    if (!this.areElementSizesAdjusted) {
+      this.adjustElementsSize();
+    }
+    if (typeof this.wot.wtViewport.rowsRenderCalculator.startPosition === 'number') {
+      this.spreader.style.top = `${this.wot.wtViewport.rowsRenderCalculator.startPosition}px`;
+
+    } else if (total === 0) {
+      // can happen if there are 0 rows
+      this.spreader.style.top = '0';
+
+    } else {
+      throw new Error('Incorrect value of the rowsRenderCalculator');
+    }
+    this.spreader.style.bottom = '';
+
+    if (this.needFullRender) {
+      this.syncOverlayOffset();
+    }
+  }
+
+  /**
+   * Synchronize calculated left position to an element.
+   */
+  syncOverlayOffset() {
+    if (typeof this.wot.wtViewport.columnsRenderCalculator.startPosition === 'number') {
+      this.clone.wtTable.spreader.style.left = `${this.wot.wtViewport.columnsRenderCalculator.startPosition}px`;
+
+    } else {
+      this.clone.wtTable.spreader.style.left = '';
+    }
+  }
+
+  /**
+   * Scrolls vertically to a row.
+   *
+   * @param {Number} sourceRow Row index which you want to scroll to.
+   * @param {Boolean} [bottomEdge] if `true`, scrolls according to the bottom edge (top edge is by default).
+   * @returns {Boolean}
+   */
+  scrollTo(sourceRow, bottomEdge) {
+    let newY = this.getTableParentOffset();
+    const sourceInstance = this.wot.cloneSource ? this.wot.cloneSource : this.wot;
+    const mainHolder = sourceInstance.wtTable.holder;
+    let scrollbarCompensation = 0;
+
+    if (bottomEdge && mainHolder.offsetHeight !== mainHolder.clientHeight) {
+      scrollbarCompensation = getScrollbarWidth();
+    }
+
+    if (bottomEdge) {
+      const fixedRowsBottom = this.wot.getSetting('fixedRowsBottom');
+      const totalRows = this.wot.getSetting('totalRows');
+
+      newY += this.sumCellSizes(0, sourceRow + 1);
+      newY -= this.wot.wtViewport.getViewportHeight() - this.sumCellSizes(totalRows - fixedRowsBottom, totalRows);
+      // Fix 1 pixel offset when cell is selected
+      newY += 1;
+
+    } else {
+      newY += this.sumCellSizes(this.wot.getSetting('fixedRowsTop'), sourceRow);
+    }
+    newY += scrollbarCompensation;
+
+    return this.setScrollPosition(newY);
+  }
+
+  /**
+   * Gets table parent top position.
+   *
+   * @returns {Number}
+   */
+  getTableParentOffset() {
+    if (this.mainTableScrollableElement === window) {
+      return this.wot.wtTable.holderOffset.top;
+
+    }
+    return 0;
+
+  }
+
+  /**
+   * Gets the main overlay's vertical scroll position.
+   *
+   * @returns {Number} Main table's vertical scroll position.
+   */
+  getScrollPosition() {
+    return getScrollTop(this.mainTableScrollableElement);
+  }
+
+  /**
+   * Redraw borders of selection.
+   *
+   * @param {WalkontableSelection} selection Selection for redraw.
+   */
+  redrawSelectionBorders(selection) {
+    if (selection && selection.cellRange) {
+      const border = selection.getBorder(this.wot);
+      const corners = selection.getCorners();
+
+      border.disappear();
+      border.appear(corners);
+    }
+  }
+
+  /**
+   * Redrawing borders of all selections.
+   */
+  redrawAllSelectionsBorders() {
+    const selections = this.wot.selections;
+
+    this.redrawSelectionBorders(selections.getCell());
+
+    arrayEach(selections.getAreas(), (area) => {
+      this.redrawSelectionBorders(area);
+    });
+    this.redrawSelectionBorders(selections.getFill());
+
+    this.wot.wtTable.wot.wtOverlays.leftOverlay.refresh();
+  }
+
+  /**
+   * Adds css classes to hide the header border's header (cell-selection border hiding issue).
+   *
+   * @param {Number} position Header Y position if trimming container is window or scroll top if not.
+   */
+  adjustHeaderBordersPosition(position) {
+    const masterParent = this.wot.wtTable.holder.parentNode;
+    const totalColumns = this.wot.getSetting('totalColumns');
+
+    if (totalColumns) {
+      removeClass(masterParent, 'emptyColumns');
+    } else {
+      addClass(masterParent, 'emptyColumns');
+    }
+
+    if (this.wot.getSetting('fixedRowsTop') === 0 && this.wot.getSetting('columnHeaders').length > 0) {
+      const previousState = hasClass(masterParent, 'innerBorderTop');
+
+      if (position || this.wot.getSetting('totalRows') === 0) {
+        addClass(masterParent, 'innerBorderTop');
+      } else {
+        removeClass(masterParent, 'innerBorderTop');
+      }
+
+      if (!previousState && position || previousState && !position) {
+        this.wot.wtOverlays.adjustElementsSize();
+
+        // cell borders should be positioned once again,
+        // because we added / removed 1px border from table header
+        this.redrawAllSelectionsBorders();
+      }
+    }
+
+    // nasty workaround for double border in the header, TODO: find a pure-css solution
+    if (this.wot.getSetting('rowHeaders').length === 0) {
+      const secondHeaderCell = this.clone.wtTable.THEAD.querySelectorAll('th:nth-of-type(2)');
+
+      if (secondHeaderCell) {
+        for (let i = 0; i < secondHeaderCell.length; i++) {
+          secondHeaderCell[i].style['border-left-width'] = 0;
+        }
+      }
+    }
+  }
+}
+
+Overlay.registerOverlay(Overlay.CLONE_TOP, TopOverlay);
+
+export default TopOverlay;

+ 78 - 0
handsontable/src/3rdparty/walkontable/src/overlay/topLeftCorner.js

@@ -0,0 +1,78 @@
+
+import {
+  outerHeight,
+  outerWidth,
+  setOverlayPosition,
+  resetCssTransform
+} from './../../../../helpers/dom/element';
+import Overlay from './_base';
+
+/**
+ * @class TopLeftCornerOverlay
+ */
+class TopLeftCornerOverlay extends Overlay {
+  /**
+   * @param {Walkontable} wotInstance
+   */
+  constructor(wotInstance) {
+    super(wotInstance);
+    this.clone = this.makeClone(Overlay.CLONE_TOP_LEFT_CORNER);
+  }
+
+  /**
+   * Checks if overlay should be fully rendered
+   *
+   * @returns {Boolean}
+   */
+  shouldBeRendered() {
+    return !!((this.wot.getSetting('fixedRowsTop') || this.wot.getSetting('columnHeaders').length) &&
+        (this.wot.getSetting('fixedColumnsLeft') || this.wot.getSetting('rowHeaders').length));
+  }
+
+  /**
+   * Updates the corner overlay position
+   */
+  resetFixedPosition() {
+    this.updateTrimmingContainer();
+
+    if (!this.wot.wtTable.holder.parentNode) {
+      // removed from DOM
+      return;
+    }
+    const overlayRoot = this.clone.wtTable.holder.parentNode;
+    const tableHeight = outerHeight(this.clone.wtTable.TABLE);
+    const tableWidth = outerWidth(this.clone.wtTable.TABLE);
+    const preventOverflow = this.wot.getSetting('preventOverflow');
+
+    if (this.trimmingContainer === window) {
+      const box = this.wot.wtTable.hider.getBoundingClientRect();
+      const top = Math.ceil(box.top);
+      const left = Math.ceil(box.left);
+      const bottom = Math.ceil(box.bottom);
+      const right = Math.ceil(box.right);
+      let finalLeft = '0';
+      let finalTop = '0';
+
+      if (!preventOverflow || preventOverflow === 'vertical') {
+        if (left < 0 && (right - overlayRoot.offsetWidth) > 0) {
+          finalLeft = `${-left}px`;
+        }
+      }
+
+      if (!preventOverflow || preventOverflow === 'horizontal') {
+        if (top < 0 && (bottom - overlayRoot.offsetHeight) > 0) {
+          finalTop = `${-top}px`;
+        }
+      }
+      setOverlayPosition(overlayRoot, finalLeft, finalTop);
+    } else {
+      resetCssTransform(overlayRoot);
+    }
+    overlayRoot.style.height = `${tableHeight === 0 ? tableHeight : tableHeight + 4}px`;
+    overlayRoot.style.width = `${tableWidth === 0 ? tableWidth : tableWidth + 4}px`;
+  }
+}
+
+Overlay.registerOverlay(Overlay.CLONE_TOP_LEFT_CORNER, TopLeftCornerOverlay);
+
+export default TopLeftCornerOverlay;

+ 586 - 0
handsontable/src/3rdparty/walkontable/src/overlays.js

@@ -0,0 +1,586 @@
+import {
+  getScrollableElement,
+  getScrollbarWidth,
+} from './../../../helpers/dom/element';
+import { arrayEach } from './../../../helpers/array';
+import { isKey } from './../../../helpers/unicode';
+import { isChrome } from './../../../helpers/browser';
+import EventManager from './../../../eventManager';
+import Overlay from './overlay/_base';
+
+/**
+ * @class Overlays
+ */
+class Overlays {
+  /**
+   * @param {Walkontable} wotInstance
+   */
+  constructor(wotInstance) {
+    /**
+     * Sometimes `line-height` might be set to 'normal'. In that case, a default `font-size` should be multiplied by roughly 1.2.
+     * https://developer.mozilla.org/pl/docs/Web/CSS/line-height#Values
+     */
+    const BODY_LINE_HEIGHT = parseInt(getComputedStyle(document.body).lineHeight, 10);
+    const FALLBACK_BODY_LINE_HEIGHT = parseInt(getComputedStyle(document.body).fontSize, 10) * 1.2;
+
+    this.wot = wotInstance;
+
+    // legacy support
+    this.instance = this.wot;
+    this.eventManager = new EventManager(this.wot);
+
+    this.wot.update('scrollbarWidth', getScrollbarWidth());
+    this.wot.update('scrollbarHeight', getScrollbarWidth());
+
+    this.scrollableElement = getScrollableElement(this.wot.wtTable.TABLE);
+
+    this.prepareOverlays();
+
+    this.destroyed = false;
+    this.keyPressed = false;
+    this.spreaderLastSize = {
+      width: null,
+      height: null,
+    };
+    this.overlayScrollPositions = {
+      master: {
+        top: 0,
+        left: 0,
+      },
+      top: {
+        top: null,
+        left: 0,
+      },
+      bottom: {
+        top: null,
+        left: 0
+      },
+      left: {
+        top: 0,
+        left: null
+      }
+    };
+
+    this.pendingScrollCallbacks = {
+      master: {
+        top: 0,
+        left: 0,
+      },
+      top: {
+        left: 0,
+      },
+      bottom: {
+        left: 0,
+      },
+      left: {
+        top: 0,
+      }
+    };
+
+    this.verticalScrolling = false;
+    this.horizontalScrolling = false;
+    this.delegatedScrollCallback = false;
+
+    this.registeredListeners = [];
+
+    this.browserLineHeight = BODY_LINE_HEIGHT || FALLBACK_BODY_LINE_HEIGHT;
+
+    this.registerListeners();
+  }
+
+  /**
+   * Prepare overlays based on user settings.
+   *
+   * @returns {Boolean} Returns `true` if changes applied to overlay needs scroll synchronization.
+   */
+  prepareOverlays() {
+    let syncScroll = false;
+
+    if (this.topOverlay) {
+      syncScroll = this.topOverlay.updateStateOfRendering() || syncScroll;
+    } else {
+      this.topOverlay = Overlay.createOverlay(Overlay.CLONE_TOP, this.wot);
+    }
+
+    if (!Overlay.hasOverlay(Overlay.CLONE_BOTTOM)) {
+      this.bottomOverlay = {
+        needFullRender: false,
+        updateStateOfRendering: () => false,
+      };
+    }
+    if (!Overlay.hasOverlay(Overlay.CLONE_BOTTOM_LEFT_CORNER)) {
+      this.bottomLeftCornerOverlay = {
+        needFullRender: false,
+        updateStateOfRendering: () => false,
+      };
+    }
+
+    if (this.bottomOverlay) {
+      syncScroll = this.bottomOverlay.updateStateOfRendering() || syncScroll;
+    } else {
+      this.bottomOverlay = Overlay.createOverlay(Overlay.CLONE_BOTTOM, this.wot);
+    }
+
+    if (this.leftOverlay) {
+      syncScroll = this.leftOverlay.updateStateOfRendering() || syncScroll;
+    } else {
+      this.leftOverlay = Overlay.createOverlay(Overlay.CLONE_LEFT, this.wot);
+    }
+
+    if (this.topOverlay.needFullRender && this.leftOverlay.needFullRender) {
+      if (this.topLeftCornerOverlay) {
+        syncScroll = this.topLeftCornerOverlay.updateStateOfRendering() || syncScroll;
+      } else {
+        this.topLeftCornerOverlay = Overlay.createOverlay(Overlay.CLONE_TOP_LEFT_CORNER, this.wot);
+      }
+    }
+
+    if (this.bottomOverlay.needFullRender && this.leftOverlay.needFullRender) {
+      if (this.bottomLeftCornerOverlay) {
+        syncScroll = this.bottomLeftCornerOverlay.updateStateOfRendering() || syncScroll;
+      } else {
+        this.bottomLeftCornerOverlay = Overlay.createOverlay(Overlay.CLONE_BOTTOM_LEFT_CORNER, this.wot);
+      }
+    }
+
+    if (this.wot.getSetting('debug') && !this.debug) {
+      this.debug = Overlay.createOverlay(Overlay.CLONE_DEBUG, this.wot);
+    }
+
+    return syncScroll;
+  }
+
+  /**
+   * Refresh and redraw table
+   */
+  refreshAll() {
+    if (!this.wot.drawn) {
+      return;
+    }
+    if (!this.wot.wtTable.holder.parentNode) {
+      // Walkontable was detached from DOM, but this handler was not removed
+      this.destroy();
+
+      return;
+    }
+    this.wot.draw(true);
+
+    if (this.verticalScrolling) {
+      this.leftOverlay.onScroll();
+    }
+
+    if (this.horizontalScrolling) {
+      this.topOverlay.onScroll();
+    }
+
+    this.verticalScrolling = false;
+    this.horizontalScrolling = false;
+  }
+
+  /**
+   * Register all necessary event listeners.
+   */
+  registerListeners() {
+    const topOverlayScrollable = this.topOverlay.mainTableScrollableElement;
+    const leftOverlayScrollable = this.leftOverlay.mainTableScrollableElement;
+
+    const listenersToRegister = [];
+    listenersToRegister.push([document.documentElement, 'keydown', event => this.onKeyDown(event)]);
+    listenersToRegister.push([document.documentElement, 'keyup', () => this.onKeyUp()]);
+    listenersToRegister.push([document, 'visibilitychange', () => this.onKeyUp()]);
+    listenersToRegister.push([topOverlayScrollable, 'scroll', event => this.onTableScroll(event)]);
+
+    if (topOverlayScrollable !== leftOverlayScrollable) {
+      listenersToRegister.push([leftOverlayScrollable, 'scroll', event => this.onTableScroll(event)]);
+    }
+
+    const isHighPixelRatio = window.devicePixelRatio && window.devicePixelRatio > 1;
+
+    if (isHighPixelRatio || !isChrome()) {
+      listenersToRegister.push([this.instance.wtTable.wtRootElement.parentNode, 'wheel', event => this.onCloneWheel(event)]);
+
+    } else {
+      if (this.topOverlay.needFullRender) {
+        listenersToRegister.push([this.topOverlay.clone.wtTable.holder, 'wheel', event => this.onCloneWheel(event)]);
+      }
+
+      if (this.bottomOverlay.needFullRender) {
+        listenersToRegister.push([this.bottomOverlay.clone.wtTable.holder, 'wheel', event => this.onCloneWheel(event)]);
+      }
+
+      if (this.leftOverlay.needFullRender) {
+        listenersToRegister.push([this.leftOverlay.clone.wtTable.holder, 'wheel', event => this.onCloneWheel(event)]);
+      }
+
+      if (this.topLeftCornerOverlay && this.topLeftCornerOverlay.needFullRender) {
+        listenersToRegister.push([this.topLeftCornerOverlay.clone.wtTable.holder, 'wheel', event => this.onCloneWheel(event)]);
+      }
+
+      if (this.bottomLeftCornerOverlay && this.bottomLeftCornerOverlay.needFullRender) {
+        listenersToRegister.push([this.bottomLeftCornerOverlay.clone.wtTable.holder, 'wheel', event => this.onCloneWheel(event)]);
+      }
+    }
+
+    if (this.topOverlay.trimmingContainer !== window && this.leftOverlay.trimmingContainer !== window) {
+      // This is necessary?
+      // eventManager.addEventListener(window, 'scroll', (event) => this.refreshAll(event));
+      listenersToRegister.push([window, 'wheel', (event) => {
+        let overlay;
+        const deltaY = event.wheelDeltaY || event.deltaY;
+        const deltaX = event.wheelDeltaX || event.deltaX;
+
+        if (this.topOverlay.clone.wtTable.holder.contains(event.realTarget)) {
+          overlay = 'top';
+
+        } else if (this.bottomOverlay.clone && this.bottomOverlay.clone.wtTable.holder.contains(event.realTarget)) {
+          overlay = 'bottom';
+
+        } else if (this.leftOverlay.clone.wtTable.holder.contains(event.realTarget)) {
+          overlay = 'left';
+
+        } else if (this.topLeftCornerOverlay && this.topLeftCornerOverlay.clone && this.topLeftCornerOverlay.clone.wtTable.holder.contains(event.realTarget)) {
+          overlay = 'topLeft';
+
+        } else if (this.bottomLeftCornerOverlay && this.bottomLeftCornerOverlay.clone && this.bottomLeftCornerOverlay.clone.wtTable.holder.contains(event.realTarget)) {
+          overlay = 'bottomLeft';
+        }
+
+        if ((overlay === 'top' && deltaY !== 0) ||
+          (overlay === 'left' && deltaX !== 0) ||
+          (overlay === 'bottom' && deltaY !== 0) ||
+          ((overlay === 'topLeft' || overlay === 'bottomLeft') && (deltaY !== 0 || deltaX !== 0))) {
+
+          event.preventDefault();
+        }
+      }]);
+    }
+
+    while (listenersToRegister.length) {
+      const listener = listenersToRegister.pop();
+      this.eventManager.addEventListener(listener[0], listener[1], listener[2]);
+
+      this.registeredListeners.push(listener);
+    }
+  }
+
+  /**
+   * Deregister all previously registered listeners.
+   */
+  deregisterListeners() {
+    while (this.registeredListeners.length) {
+      const listener = this.registeredListeners.pop();
+      this.eventManager.removeEventListener(listener[0], listener[1], listener[2]);
+    }
+  }
+
+  /**
+   * Scroll listener
+   *
+   * @param {Event} event
+   */
+  onTableScroll(event) {
+    // There was if statement which controlled flow of this function. It avoided the execution of the next lines
+    // on mobile devices. It was changed. Broader description of this case is included within issue #4856.
+
+    const masterHorizontal = this.leftOverlay.mainTableScrollableElement;
+    const masterVertical = this.topOverlay.mainTableScrollableElement;
+    const target = event.target;
+
+    // For key press, sync only master -> overlay position because while pressing Walkontable.render is triggered
+    // by hot.refreshBorder
+    if (this.keyPressed) {
+      if ((masterVertical !== window && target !== window && !event.target.contains(masterVertical)) ||
+          (masterHorizontal !== window && target !== window && !event.target.contains(masterHorizontal))) {
+        return;
+      }
+    }
+
+    this.syncScrollPositions(event);
+  }
+
+  /**
+   * Wheel listener for cloned overlays.
+   *
+   * @param {Event} event
+   */
+  onCloneWheel(event) {
+    if (this.scrollableElement !== window) {
+      event.preventDefault();
+    }
+    // There was if statement which controlled flow of this function. It avoided the execution of the next lines
+    // on mobile devices. It was changed. Broader description of this case is included within issue #4856.
+
+    const masterHorizontal = this.leftOverlay.mainTableScrollableElement;
+    const masterVertical = this.topOverlay.mainTableScrollableElement;
+    const target = event.target;
+
+    // For key press, sync only master -> overlay position because while pressing Walkontable.render is triggered
+    // by hot.refreshBorder
+    const shouldNotWheelVertically = masterVertical !== window && target !== window && !event.target.contains(masterVertical);
+    const shouldNotWheelHorizontally = masterHorizontal !== window && target !== window && !event.target.contains(masterHorizontal);
+
+    if (this.keyPressed && (shouldNotWheelVertically || shouldNotWheelHorizontally)) {
+      return;
+    }
+
+    this.translateMouseWheelToScroll(event);
+  }
+
+  /**
+   * Key down listener
+   */
+  onKeyDown(event) {
+    this.keyPressed = isKey(event.keyCode, 'ARROW_UP|ARROW_RIGHT|ARROW_DOWN|ARROW_LEFT');
+  }
+
+  /**
+   * Key up listener
+   */
+  onKeyUp() {
+    this.keyPressed = false;
+  }
+
+  /**
+   * Translate wheel event into scroll event and sync scroll overlays position
+   *
+   * @private
+   * @param {Event} event
+   * @returns {Boolean}
+   */
+  translateMouseWheelToScroll(event) {
+    let deltaY = isNaN(event.deltaY) ? (-1) * event.wheelDeltaY : event.deltaY;
+    let deltaX = isNaN(event.deltaX) ? (-1) * event.wheelDeltaX : event.deltaX;
+
+    if (event.deltaMode === 1) {
+      deltaX += deltaX * this.browserLineHeight;
+      deltaY += deltaY * this.browserLineHeight;
+    }
+
+    this.scrollVertically(deltaY);
+    this.scrollHorizontally(deltaX);
+
+    return false;
+  }
+
+  scrollVertically(distance) {
+    if (distance === 0) {
+      return 0;
+    }
+    this.scrollableElement.scrollTop += distance;
+  }
+
+  scrollHorizontally(distance) {
+    if (distance === 0) {
+      return 0;
+    }
+    this.scrollableElement.scrollLeft += distance;
+  }
+
+  /**
+   * Synchronize scroll position between master table and overlay table.
+   *
+   * @private
+   * @param {Event|Object} event
+   */
+  syncScrollPositions() {
+    if (this.destroyed) {
+      return;
+    }
+
+    const topHolder = this.topOverlay.clone.wtTable.holder;
+    const leftHolder = this.leftOverlay.clone.wtTable.holder;
+
+    const [scrollLeft, scrollTop] = [this.scrollableElement.scrollLeft, this.scrollableElement.scrollTop];
+
+    this.horizontalScrolling = topHolder.scrollLeft !== scrollLeft;
+    this.verticalScrolling = leftHolder.scrollTop !== scrollTop;
+
+    if (this.horizontalScrolling) {
+      topHolder.scrollLeft = scrollLeft;
+
+      const bottomHolder = this.bottomOverlay.needFullRender ? this.bottomOverlay.clone.wtTable.holder : null;
+
+      if (bottomHolder) {
+        bottomHolder.scrollLeft = scrollLeft;
+      }
+    }
+
+    if (this.verticalScrolling) {
+      leftHolder.scrollTop = scrollTop;
+    }
+
+    this.refreshAll();
+  }
+
+  /**
+   * Synchronize overlay scrollbars with the master scrollbar
+   */
+  syncScrollWithMaster() {
+    const master = this.topOverlay.mainTableScrollableElement;
+    const { scrollLeft, scrollTop } = master;
+
+    if (this.topOverlay.needFullRender) {
+      this.topOverlay.clone.wtTable.holder.scrollLeft = scrollLeft;
+    }
+    if (this.bottomOverlay.needFullRender) {
+      this.bottomOverlay.clone.wtTable.holder.scrollLeft = scrollLeft;
+    }
+    if (this.leftOverlay.needFullRender) {
+      this.leftOverlay.clone.wtTable.holder.scrollTop = scrollTop;
+    }
+  }
+
+  /**
+   * Update the main scrollable elements for all the overlays.
+   */
+  updateMainScrollableElements() {
+    this.deregisterListeners();
+
+    this.leftOverlay.updateMainScrollableElement();
+    this.topOverlay.updateMainScrollableElement();
+
+    if (this.bottomOverlay.needFullRender) {
+      this.bottomOverlay.updateMainScrollableElement();
+    }
+
+    this.scrollableElement = getScrollableElement(this.wot.wtTable.TABLE);
+
+    this.registerListeners();
+  }
+
+  /**
+   *
+   */
+  destroy() {
+    this.eventManager.destroy();
+    this.topOverlay.destroy();
+
+    if (this.bottomOverlay.clone) {
+      this.bottomOverlay.destroy();
+    }
+    this.leftOverlay.destroy();
+
+    if (this.topLeftCornerOverlay) {
+      this.topLeftCornerOverlay.destroy();
+    }
+
+    if (this.bottomLeftCornerOverlay && this.bottomLeftCornerOverlay.clone) {
+      this.bottomLeftCornerOverlay.destroy();
+    }
+
+    if (this.debug) {
+      this.debug.destroy();
+    }
+    this.destroyed = true;
+  }
+
+  /**
+   * @param {Boolean} [fastDraw=false]
+   */
+  refresh(fastDraw = false) {
+    if (this.topOverlay.areElementSizesAdjusted && this.leftOverlay.areElementSizesAdjusted) {
+      const container = this.wot.wtTable.wtRootElement.parentNode || this.wot.wtTable.wtRootElement;
+      const width = container.clientWidth;
+      const height = container.clientHeight;
+
+      if (width !== this.spreaderLastSize.width || height !== this.spreaderLastSize.height) {
+        this.spreaderLastSize.width = width;
+        this.spreaderLastSize.height = height;
+        this.adjustElementsSize();
+      }
+    }
+
+    if (this.bottomOverlay.clone) {
+      this.bottomOverlay.refresh(fastDraw);
+    }
+
+    this.leftOverlay.refresh(fastDraw);
+    this.topOverlay.refresh(fastDraw);
+
+    if (this.topLeftCornerOverlay) {
+      this.topLeftCornerOverlay.refresh(fastDraw);
+    }
+
+    if (this.bottomLeftCornerOverlay && this.bottomLeftCornerOverlay.clone) {
+      this.bottomLeftCornerOverlay.refresh(fastDraw);
+    }
+
+    if (this.debug) {
+      this.debug.refresh(fastDraw);
+    }
+  }
+
+  /**
+   * Adjust overlays elements size and master table size
+   *
+   * @param {Boolean} [force=false]
+   */
+  adjustElementsSize(force = false) {
+    const totalColumns = this.wot.getSetting('totalColumns');
+    const totalRows = this.wot.getSetting('totalRows');
+    const headerRowSize = this.wot.wtViewport.getRowHeaderWidth();
+    const headerColumnSize = this.wot.wtViewport.getColumnHeaderHeight();
+    const hiderStyle = this.wot.wtTable.hider.style;
+
+    hiderStyle.width = `${headerRowSize + this.leftOverlay.sumCellSizes(0, totalColumns)}px`;
+    hiderStyle.height = `${headerColumnSize + this.topOverlay.sumCellSizes(0, totalRows) + 1}px`;
+
+    this.topOverlay.adjustElementsSize(force);
+    this.leftOverlay.adjustElementsSize(force);
+
+    if (this.bottomOverlay.clone) {
+      this.bottomOverlay.adjustElementsSize(force);
+    }
+  }
+
+  /**
+   *
+   */
+  applyToDOM() {
+    if (!this.topOverlay.areElementSizesAdjusted || !this.leftOverlay.areElementSizesAdjusted) {
+      this.adjustElementsSize();
+    }
+    this.topOverlay.applyToDOM();
+
+    if (this.bottomOverlay.clone) {
+      this.bottomOverlay.applyToDOM();
+    }
+
+    this.leftOverlay.applyToDOM();
+  }
+
+  /**
+   * Get the parent overlay of the provided element.
+   *
+   * @param {HTMLElement} element
+   * @returns {Object|null}
+   */
+  getParentOverlay(element) {
+    if (!element) {
+      return null;
+    }
+
+    const overlays = [
+      this.topOverlay,
+      this.leftOverlay,
+      this.bottomOverlay,
+      this.topLeftCornerOverlay,
+      this.bottomLeftCornerOverlay
+    ];
+    let result = null;
+
+    arrayEach(overlays, (elem) => {
+      if (!elem) {
+        return;
+      }
+
+      if (elem.clone && elem.clone.wtTable.TABLE.contains(element)) {
+        result = elem.clone;
+      }
+    });
+
+    return result;
+  }
+}
+
+export default Overlays;

+ 301 - 0
handsontable/src/3rdparty/walkontable/src/scroll.js

@@ -0,0 +1,301 @@
+import {
+  innerHeight,
+  innerWidth,
+  getScrollLeft,
+  getScrollTop,
+  offset,
+} from './../../../helpers/dom/element';
+import { rangeEach, rangeEachReverse } from './../../../helpers/number';
+
+/**
+ * @class Scroll
+ */
+class Scroll {
+  /**
+   * @param {Walkontable} wotInstance
+   */
+  constructor(wotInstance) {
+    this.wot = wotInstance;
+
+    // legacy support
+    this.instance = wotInstance;
+  }
+
+  /**
+   * Scrolls viewport to a cell.
+   *
+   * @param {CellCoords} coords
+   * @param {Boolean} [snapToTop]
+   * @param {Boolean} [snapToRight]
+   * @param {Boolean} [snapToBottom]
+   * @param {Boolean} [snapToLeft]
+   * @returns {Boolean}
+   */
+  scrollViewport(coords, snapToTop, snapToRight, snapToBottom, snapToLeft) {
+    const scrolledHorizontally = this.scrollViewportHorizontally(coords.col, snapToRight, snapToLeft);
+    const scrolledVertically = this.scrollViewportVertically(coords.row, snapToTop, snapToBottom);
+
+    return scrolledHorizontally || scrolledVertically;
+  }
+
+  /**
+   * Scrolls viewport to a column.
+   *
+   * @param {Number} column Visual column index.
+   * @param {Boolean} [snapToRight]
+   * @param {Boolean} [snapToLeft]
+   * @returns {Boolean}
+   */
+  scrollViewportHorizontally(column, snapToRight, snapToLeft) {
+    if (!this.wot.drawn) {
+      return false;
+    }
+
+    const {
+      fixedColumnsLeft,
+      leftOverlay,
+      totalColumns,
+    } = this._getVariables();
+    let result = false;
+
+    if (column >= 0 && column <= Math.max(totalColumns - 1, 0)) {
+      if (column >= fixedColumnsLeft && (column < this.getFirstVisibleColumn() || snapToLeft)) {
+        result = leftOverlay.scrollTo(column);
+      } else if (column > this.getLastVisibleColumn() || snapToRight) {
+        result = leftOverlay.scrollTo(column, true);
+      }
+    }
+
+    return result;
+  }
+
+  /**
+   * Scrolls viewport to a row.
+   *
+   * @param {Number} row Visual row index.
+   * @param {Boolean} [snapToTop]
+   * @param {Boolean} [snapToBottom]
+   * @returns {Boolean}
+   */
+  scrollViewportVertically(row, snapToTop, snapToBottom) {
+    if (!this.wot.drawn) {
+      return false;
+    }
+
+    const {
+      fixedRowsBottom,
+      fixedRowsTop,
+      topOverlay,
+      totalRows,
+    } = this._getVariables();
+    let result = false;
+
+    if (row >= 0 && row <= Math.max(totalRows - 1, 0)) {
+      if (row >= fixedRowsTop && (row < this.getFirstVisibleRow() || snapToTop)) {
+        result = topOverlay.scrollTo(row);
+      } else if ((row > this.getLastVisibleRow() && row < totalRows - fixedRowsBottom) || snapToBottom) {
+        result = topOverlay.scrollTo(row, true);
+      }
+    }
+
+    return result;
+  }
+
+  /**
+   * Get first visible row based on virtual dom and how table is visible in browser window viewport.
+   *
+   * @returns {Number}
+   */
+  getFirstVisibleRow() {
+    const {
+      topOverlay,
+      wtTable,
+      wtViewport,
+      totalRows,
+      fixedRowsTop,
+    } = this._getVariables();
+
+    let firstVisibleRow = wtTable.getFirstVisibleRow();
+
+    if (topOverlay.mainTableScrollableElement === window) {
+      const rootElementOffset = offset(wtTable.wtRootElement);
+      const totalTableHeight = innerHeight(wtTable.hider);
+      const windowHeight = innerHeight(window);
+      const windowScrollTop = getScrollTop(window);
+
+      // Only calculate firstVisibleRow when table didn't filled (from up) whole viewport space
+      if (rootElementOffset.top + totalTableHeight - windowHeight <= windowScrollTop) {
+        let rowsHeight = wtViewport.getColumnHeaderHeight();
+
+        rowsHeight += topOverlay.sumCellSizes(0, fixedRowsTop);
+
+        rangeEachReverse(totalRows, 1, (row) => {
+          rowsHeight += topOverlay.sumCellSizes(row - 1, row);
+
+          if (rootElementOffset.top + totalTableHeight - rowsHeight <= windowScrollTop) {
+            // Return physical row + 1
+            firstVisibleRow = row;
+
+            return false;
+          }
+        });
+      }
+    }
+
+    return firstVisibleRow;
+  }
+
+  /**
+   * Get last visible row based on virtual dom and how table is visible in browser window viewport.
+   *
+   * @returns {Number}
+   */
+  getLastVisibleRow() {
+    const {
+      topOverlay,
+      wtTable,
+      wtViewport,
+      totalRows,
+    } = this._getVariables();
+
+    let lastVisibleRow = wtTable.getLastVisibleRow();
+
+    if (topOverlay.mainTableScrollableElement === window) {
+      const rootElementOffset = offset(wtTable.wtRootElement);
+      const windowHeight = innerHeight(window);
+      const windowScrollTop = getScrollTop(window);
+
+      // Only calculate lastVisibleRow when table didn't filled (from bottom) whole viewport space
+      if (rootElementOffset.top > windowScrollTop) {
+        let rowsHeight = wtViewport.getColumnHeaderHeight();
+
+        rangeEach(1, totalRows, (row) => {
+          rowsHeight += topOverlay.sumCellSizes(row - 1, row);
+
+          if (rootElementOffset.top + rowsHeight - windowScrollTop >= windowHeight) {
+            // Return physical row - 1 (-2 because rangeEach gives row index + 1 - sumCellSizes requirements)
+            lastVisibleRow = row - 2;
+
+            return false;
+          }
+        });
+      }
+    }
+
+    return lastVisibleRow;
+  }
+
+  /**
+   * Get first visible column based on virtual dom and how table is visible in browser window viewport.
+   *
+   * @returns {Number}
+   */
+  getFirstVisibleColumn() {
+    const {
+      leftOverlay,
+      wtTable,
+      wtViewport,
+      totalColumns,
+    } = this._getVariables();
+
+    let firstVisibleColumn = wtTable.getFirstVisibleColumn();
+
+    if (leftOverlay.mainTableScrollableElement === window) {
+      const rootElementOffset = offset(wtTable.wtRootElement);
+      const totalTableWidth = innerWidth(wtTable.hider);
+      const windowWidth = innerWidth(window);
+      const windowScrollLeft = getScrollLeft(window);
+
+      // Only calculate firstVisibleColumn when table didn't filled (from left) whole viewport space
+      if (rootElementOffset.left + totalTableWidth - windowWidth <= windowScrollLeft) {
+        let columnsWidth = wtViewport.getRowHeaderWidth();
+
+        rangeEachReverse(totalColumns, 1, (column) => {
+          columnsWidth += leftOverlay.sumCellSizes(column - 1, column);
+
+          if (rootElementOffset.left + totalTableWidth - columnsWidth <= windowScrollLeft) {
+            // Return physical column + 1
+            firstVisibleColumn = column;
+
+            return false;
+          }
+        });
+      }
+    }
+
+    return firstVisibleColumn;
+  }
+
+  /**
+   * Get last visible column based on virtual dom and how table is visible in browser window viewport.
+   *
+   * @returns {Number}
+   */
+  getLastVisibleColumn() {
+    const {
+      leftOverlay,
+      wtTable,
+      wtViewport,
+      totalColumns,
+    } = this._getVariables();
+
+    let lastVisibleColumn = wtTable.getLastVisibleColumn();
+
+    if (leftOverlay.mainTableScrollableElement === window) {
+      const rootElementOffset = offset(wtTable.wtRootElement);
+      const windowWidth = innerWidth(window);
+      const windowScrollLeft = getScrollLeft(window);
+
+      // Only calculate lastVisibleColumn when table didn't filled (from right) whole viewport space
+      if (rootElementOffset.left > windowScrollLeft) {
+        let columnsWidth = wtViewport.getRowHeaderWidth();
+
+        rangeEach(1, totalColumns, (column) => {
+          columnsWidth += leftOverlay.sumCellSizes(column - 1, column);
+
+          if (rootElementOffset.left + columnsWidth - windowScrollLeft >= windowWidth) {
+            // Return physical column - 1 (-2 because rangeEach gives column index + 1 - sumCellSizes requirements)
+            lastVisibleColumn = column - 2;
+
+            return false;
+          }
+        });
+      }
+    }
+
+    return lastVisibleColumn;
+  }
+
+  /**
+   * Returns collection of variables used to rows and columns visibility calculations.
+   *
+   * @returns {Object}
+   * @private
+   */
+  _getVariables() {
+    const wot = this.wot;
+    const topOverlay = wot.wtOverlays.topOverlay;
+    const leftOverlay = wot.wtOverlays.leftOverlay;
+    const wtTable = wot.wtTable;
+    const wtViewport = wot.wtViewport;
+    const totalRows = wot.getSetting('totalRows');
+    const totalColumns = wot.getSetting('totalColumns');
+    const fixedRowsTop = wot.getSetting('fixedRowsTop');
+    const fixedRowsBottom = wot.getSetting('fixedRowsBottom');
+    const fixedColumnsLeft = wot.getSetting('fixedColumnsLeft');
+
+    return {
+      topOverlay,
+      leftOverlay,
+      wtTable,
+      wtViewport,
+      totalRows,
+      totalColumns,
+      fixedRowsTop,
+      fixedRowsBottom,
+      fixedColumnsLeft
+    };
+  }
+}
+
+export default Scroll;

+ 282 - 0
handsontable/src/3rdparty/walkontable/src/selection.js

@@ -0,0 +1,282 @@
+import { addClass, hasClass } from './../../../helpers/dom/element';
+import Border from './border';
+import CellCoords from './cell/coords';
+import CellRange from './cell/range';
+
+/**
+ * @class Selection
+ */
+class Selection {
+  /**
+   * @param {Object} settings
+   * @param {CellRange} cellRange
+   */
+  constructor(settings, cellRange) {
+    this.settings = settings;
+    this.cellRange = cellRange || null;
+    this.instanceBorders = {};
+    this.classNames = [this.settings.className];
+    this.classNameGenerator = this.linearClassNameGenerator(this.settings.className, this.settings.layerLevel);
+  }
+
+  /**
+   * Each Walkontable clone requires it's own border for every selection. This method creates and returns selection
+   * borders per instance
+   *
+   * @param {Walkontable} wotInstance
+   * @returns {Border}
+   */
+  getBorder(wotInstance) {
+    if (!this.instanceBorders[wotInstance.guid]) {
+      this.instanceBorders[wotInstance.guid] = new Border(wotInstance, this.settings);
+    }
+
+    return this.instanceBorders[wotInstance.guid];
+  }
+
+  /**
+   * Checks if selection is empty
+   *
+   * @returns {Boolean}
+   */
+  isEmpty() {
+    return this.cellRange === null;
+  }
+
+  /**
+   * Adds a cell coords to the selection
+   *
+   * @param {CellCoords} coords
+   */
+  add(coords) {
+    if (this.isEmpty()) {
+      this.cellRange = new CellRange(coords);
+
+    } else {
+      this.cellRange.expand(coords);
+    }
+
+    return this;
+  }
+
+  /**
+   * If selection range from or to property equals oldCoords, replace it with newCoords. Return boolean
+   * information about success
+   *
+   * @param {CellCoords} oldCoords
+   * @param {CellCoords} newCoords
+   * @returns {Boolean}
+   */
+  replace(oldCoords, newCoords) {
+    if (!this.isEmpty()) {
+      if (this.cellRange.from.isEqual(oldCoords)) {
+        this.cellRange.from = newCoords;
+
+        return true;
+      }
+      if (this.cellRange.to.isEqual(oldCoords)) {
+        this.cellRange.to = newCoords;
+
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * Clears selection
+   *
+   * @returns {Selection}
+   */
+  clear() {
+    this.cellRange = null;
+
+    return this;
+  }
+
+  /**
+   * Returns the top left (TL) and bottom right (BR) selection coordinates
+   *
+   * @returns {Array} Returns array of coordinates for example `[1, 1, 5, 5]`
+   */
+  getCorners() {
+    const topLeft = this.cellRange.getTopLeftCorner();
+    const bottomRight = this.cellRange.getBottomRightCorner();
+
+    return [
+      topLeft.row,
+      topLeft.col,
+      bottomRight.row,
+      bottomRight.col,
+    ];
+  }
+
+  /**
+   * Adds class name to cell element at given coords
+   *
+   * @param {Walkontable} wotInstance Walkontable instance
+   * @param {Number} sourceRow Cell row coord
+   * @param {Number} sourceColumn Cell column coord
+   * @param {String} className Class name
+   * @param {Boolean} [markIntersections=false] If `true`, linear className generator will be used to add CSS classes
+   *                                            in a continuous way.
+   * @returns {Selection}
+   */
+  addClassAtCoords(wotInstance, sourceRow, sourceColumn, className, markIntersections = false) {
+    const TD = wotInstance.wtTable.getCell(new CellCoords(sourceRow, sourceColumn));
+
+    if (typeof TD === 'object') {
+      let cellClassName = className;
+
+      if (markIntersections) {
+        cellClassName = this.classNameGenerator(TD);
+
+        if (!this.classNames.includes(cellClassName)) {
+          this.classNames.push(cellClassName);
+        }
+      }
+
+      addClass(TD, cellClassName);
+    }
+
+    return this;
+  }
+
+  /**
+   * Generate helper for calculating classNames based on previously added base className.
+   * The generated className is always generated as a continuation of the previous className. For example, when
+   * the currently checked element has 'area-2' className the generated new className will be 'area-3'. When
+   * the element doesn't have any classNames than the base className will be returned ('area');
+   *
+   * @param {String} baseClassName Base className to be used.
+   * @param {Number} layerLevelOwner Layer level which the instance of the Selection belongs to.
+   * @return {Function}
+   */
+  linearClassNameGenerator(baseClassName, layerLevelOwner) {
+    // TODO: Make this recursive function Proper Tail Calls (TCO/PTC) friendly.
+    return function calcClassName(element, previousIndex = -1) {
+      if (layerLevelOwner === 0 || previousIndex === 0) {
+        return baseClassName;
+      }
+
+      let index = previousIndex >= 0 ? previousIndex : layerLevelOwner;
+      let className = baseClassName;
+
+      index -= 1;
+
+      const previousClassName = index === 0 ? baseClassName : `${baseClassName}-${index}`;
+
+      if (hasClass(element, previousClassName)) {
+        const currentLayer = index + 1;
+
+        className = `${baseClassName}-${currentLayer}`;
+
+      } else {
+        className = calcClassName(element, index);
+      }
+
+      return className;
+    };
+  }
+
+  /**
+   * @param wotInstance
+   */
+  draw(wotInstance) {
+    if (this.isEmpty()) {
+      if (this.settings.border) {
+        this.getBorder(wotInstance).disappear();
+      }
+
+      return;
+    }
+
+    const renderedRows = wotInstance.wtTable.getRenderedRowsCount();
+    const renderedColumns = wotInstance.wtTable.getRenderedColumnsCount();
+    const corners = this.getCorners();
+    const [topRow, topColumn, bottomRow, bottomColumn] = corners;
+
+    for (let column = 0; column < renderedColumns; column += 1) {
+      const sourceCol = wotInstance.wtTable.columnFilter.renderedToSource(column);
+
+      if (sourceCol >= topColumn && sourceCol <= bottomColumn) {
+        const TH = wotInstance.wtTable.getColumnHeader(sourceCol);
+
+        if (TH) {
+          const newClasses = [];
+
+          if (this.settings.highlightHeaderClassName) {
+            newClasses.push(this.settings.highlightHeaderClassName);
+          }
+
+          if (this.settings.highlightColumnClassName) {
+            newClasses.push(this.settings.highlightColumnClassName);
+          }
+
+          addClass(TH, newClasses);
+        }
+      }
+    }
+
+    for (let row = 0; row < renderedRows; row += 1) {
+      const sourceRow = wotInstance.wtTable.rowFilter.renderedToSource(row);
+
+      if (sourceRow >= topRow && sourceRow <= bottomRow) {
+        const TH = wotInstance.wtTable.getRowHeader(sourceRow);
+
+        if (TH) {
+          const newClasses = [];
+
+          if (this.settings.highlightHeaderClassName) {
+            newClasses.push(this.settings.highlightHeaderClassName);
+          }
+
+          if (this.settings.highlightRowClassName) {
+            newClasses.push(this.settings.highlightRowClassName);
+          }
+
+          addClass(TH, newClasses);
+        }
+      }
+
+      for (let column = 0; column < renderedColumns; column += 1) {
+        const sourceCol = wotInstance.wtTable.columnFilter.renderedToSource(column);
+
+        if (sourceRow >= topRow && sourceRow <= bottomRow && sourceCol >= topColumn && sourceCol <= bottomColumn) {
+          // selected cell
+          if (this.settings.className) {
+            this.addClassAtCoords(wotInstance, sourceRow, sourceCol, this.settings.className, this.settings.markIntersections);
+          }
+
+        } else if (sourceRow >= topRow && sourceRow <= bottomRow) {
+          // selection is in this row
+          if (this.settings.highlightRowClassName) {
+            this.addClassAtCoords(wotInstance, sourceRow, sourceCol, this.settings.highlightRowClassName);
+          }
+        } else if (sourceCol >= topColumn && sourceCol <= bottomColumn) {
+          // selection is in this column
+          if (this.settings.highlightColumnClassName) {
+            this.addClassAtCoords(wotInstance, sourceRow, sourceCol, this.settings.highlightColumnClassName);
+          }
+        }
+
+        const additionalSelectionClass = wotInstance.getSetting('onAfterDrawSelection', sourceRow, sourceCol, corners, this.settings.layerLevel);
+
+        if (typeof additionalSelectionClass === 'string') {
+          this.addClassAtCoords(wotInstance, sourceRow, sourceCol, additionalSelectionClass);
+        }
+
+      }
+    }
+
+    wotInstance.getSetting('onBeforeDrawBorders', corners, this.settings.className);
+
+    if (this.settings.border) {
+      // warning! border.appear modifies corners!
+      this.getBorder(wotInstance).appear(corners);
+    }
+  }
+}
+
+export default Selection;

+ 175 - 0
handsontable/src/3rdparty/walkontable/src/settings.js

@@ -0,0 +1,175 @@
+import { fastInnerText } from './../../../helpers/dom/element';
+import { objectEach } from './../../../helpers/object';
+
+/**
+ * @class Settings
+ */
+class Settings {
+  /**
+   * @param {Walkontable} wotInstance
+   * @param {Object} settings
+   */
+  constructor(wotInstance, settings) {
+    this.wot = wotInstance;
+
+    // legacy support
+    this.instance = wotInstance;
+
+    // default settings. void 0 means it is required, null means it can be empty
+    this.defaults = {
+      table: void 0,
+      debug: false, // shows WalkontableDebugOverlay
+
+      // presentation mode
+      externalRowCalculator: false,
+      stretchH: 'none', // values: all, last, none
+      currentRowClassName: null,
+      currentColumnClassName: null,
+      preventOverflow() {
+        return false;
+      },
+
+      // data source
+      data: void 0,
+      freezeOverlays: false,
+      fixedColumnsLeft: 0,
+      fixedRowsTop: 0,
+      fixedRowsBottom: 0,
+      minSpareRows: 0,
+
+      // this must be array of functions: [function (row, TH) {}]
+      rowHeaders() {
+        return [];
+      },
+
+      // this must be array of functions: [function (column, TH) {}]
+      columnHeaders() {
+        return [];
+      },
+      totalRows: void 0,
+      totalColumns: void 0,
+      cellRenderer: (row, column, TD) => {
+        const cellData = this.getSetting('data', row, column);
+
+        fastInnerText(TD, cellData === void 0 || cellData === null ? '' : cellData);
+      },
+
+      // columnWidth: 50,
+      columnWidth() {
+        // return undefined means use default size for the rendered cell content
+      },
+      rowHeight() {
+        // return undefined means use default size for the rendered cell content
+      },
+      defaultRowHeight: 23,
+      defaultColumnWidth: 50,
+      selections: null,
+      hideBorderOnMouseDownOver: false,
+      viewportRowCalculatorOverride: null,
+      viewportColumnCalculatorOverride: null,
+
+      // callbacks
+      onCellMouseDown: null,
+      onCellContextMenu: null,
+      onCellMouseOver: null,
+      onCellMouseOut: null,
+      onCellMouseUp: null,
+
+      //    onCellMouseOut: null,
+      onCellDblClick: null,
+      onCellCornerMouseDown: null,
+      onCellCornerDblClick: null,
+      beforeDraw: null,
+      onDraw: null,
+      onBeforeRemoveCellClassNames: null,
+      onAfterDrawSelection: null,
+      onBeforeDrawBorders: null,
+      onScrollVertically: null,
+      onScrollHorizontally: null,
+      onBeforeTouchScroll: null,
+      onAfterMomentumScroll: null,
+      onBeforeStretchingColumnWidth: width => width,
+      onModifyRowHeaderWidth: null,
+      onModifyGetCellCoords: null,
+
+      // constants
+      scrollbarWidth: 10,
+      scrollbarHeight: 10,
+
+      renderAllRows: false,
+      groups: false,
+      rowHeaderWidth: null,
+      columnHeaderHeight: null,
+      headerClassName: null
+    };
+
+    // reference to settings
+    this.settings = {};
+
+    objectEach(this.defaults, (value, key) => {
+      if (settings[key] !== void 0) {
+        this.settings[key] = settings[key];
+
+      } else if (value === void 0) {
+        throw new Error(`A required setting "${key}" was not provided`);
+
+      } else {
+        this.settings[key] = value;
+      }
+    });
+  }
+
+  /**
+   * Update settings
+   *
+   * @param {Object} settings
+   * @param {*} value
+   * @returns {Walkontable}
+   */
+  update(settings, value) {
+    if (value === void 0) { // settings is object
+      objectEach(settings, (settingValue, key) => {
+        this.settings[key] = settingValue;
+      });
+    } else { // if value is defined then settings is the key
+      this.settings[settings] = value;
+    }
+    return this.wot;
+  }
+
+  /**
+   * Get setting by name
+   *
+   * @param {String} key
+   * @param {*} param1
+   * @param {*} param2
+   * @param {*} param3
+   * @param {*} param4
+   * @returns {*}
+   */
+  getSetting(key, param1, param2, param3, param4) {
+    if (typeof this.settings[key] === 'function') {
+      // this is faster than .apply - https://github.com/handsontable/handsontable/wiki/JavaScript-&-DOM-performance-tips
+      return this.settings[key](param1, param2, param3, param4);
+
+    } else if (param1 !== void 0 && Array.isArray(this.settings[key])) {
+      // perhaps this can be removed, it is only used in tests
+      return this.settings[key][param1];
+
+    }
+
+    return this.settings[key];
+  }
+
+  /**
+   * Checks if setting exists
+   *
+   * @param {Boolean} key
+   * @returns {Boolean}
+   */
+  has(key) {
+    return !!this.settings[key];
+  }
+}
+
+export default Settings;

+ 695 - 0
handsontable/src/3rdparty/walkontable/src/table.js

@@ -0,0 +1,695 @@
+import {
+  getStyle,
+  getTrimmingContainer,
+  hasClass,
+  index,
+  offset,
+  removeClass,
+  removeTextNodes,
+  overlayContainsElement,
+  closest
+} from './../../../helpers/dom/element';
+import { isFunction } from './../../../helpers/function';
+import CellCoords from './cell/coords';
+import ColumnFilter from './filter/column';
+import RowFilter from './filter/row';
+import TableRenderer from './tableRenderer';
+import Overlay from './overlay/_base';
+
+/**
+ *
+ */
+class Table {
+  /**
+   * @param {Walkontable} wotInstance
+   * @param {HTMLTableElement} table
+   */
+  constructor(wotInstance, table) {
+    this.wot = wotInstance;
+
+    // legacy support
+    this.instance = this.wot;
+    this.TABLE = table;
+    this.TBODY = null;
+    this.THEAD = null;
+    this.COLGROUP = null;
+    this.tableOffset = 0;
+    this.holderOffset = 0;
+
+    removeTextNodes(this.TABLE);
+
+    this.spreader = this.createSpreader(this.TABLE);
+    this.hider = this.createHider(this.spreader);
+    this.holder = this.createHolder(this.hider);
+
+    this.wtRootElement = this.holder.parentNode;
+    this.alignOverlaysWithTrimmingContainer();
+    this.fixTableDomTree();
+
+    this.colgroupChildrenLength = this.COLGROUP.childNodes.length;
+    this.theadChildrenLength = this.THEAD.firstChild ? this.THEAD.firstChild.childNodes.length : 0;
+    this.tbodyChildrenLength = this.TBODY.childNodes.length;
+
+    this.rowFilter = null;
+    this.columnFilter = null;
+    this.correctHeaderWidth = false;
+
+    const origRowHeaderWidth = this.wot.wtSettings.settings.rowHeaderWidth;
+
+    // Fix for jumping row headers (https://github.com/handsontable/handsontable/issues/3850)
+    this.wot.wtSettings.settings.rowHeaderWidth = () => this._modifyRowHeaderWidth(origRowHeaderWidth);
+  }
+
+  /**
+   *
+   */
+  fixTableDomTree() {
+    this.TBODY = this.TABLE.querySelector('tbody');
+
+    if (!this.TBODY) {
+      this.TBODY = document.createElement('tbody');
+      this.TABLE.appendChild(this.TBODY);
+    }
+    this.THEAD = this.TABLE.querySelector('thead');
+
+    if (!this.THEAD) {
+      this.THEAD = document.createElement('thead');
+      this.TABLE.insertBefore(this.THEAD, this.TBODY);
+    }
+    this.COLGROUP = this.TABLE.querySelector('colgroup');
+
+    if (!this.COLGROUP) {
+      this.COLGROUP = document.createElement('colgroup');
+      this.TABLE.insertBefore(this.COLGROUP, this.THEAD);
+    }
+
+    if (this.wot.getSetting('columnHeaders').length && !this.THEAD.childNodes.length) {
+      this.THEAD.appendChild(document.createElement('TR'));
+    }
+  }
+
+  /**
+   * @param table
+   * @returns {HTMLElement}
+   */
+  createSpreader(table) {
+    const parent = table.parentNode;
+    let spreader;
+
+    if (!parent || parent.nodeType !== 1 || !hasClass(parent, 'wtHolder')) {
+      spreader = document.createElement('div');
+      spreader.className = 'wtSpreader';
+
+      if (parent) {
+        // if TABLE is detached (e.g. in Jasmine test), it has no parentNode so we cannot attach holder to it
+        parent.insertBefore(spreader, table);
+      }
+      spreader.appendChild(table);
+    }
+    spreader.style.position = 'relative';
+
+    return spreader;
+  }
+
+  /**
+   * @param spreader
+   * @returns {HTMLElement}
+   */
+  createHider(spreader) {
+    const parent = spreader.parentNode;
+    let hider;
+
+    if (!parent || parent.nodeType !== 1 || !hasClass(parent, 'wtHolder')) {
+      hider = document.createElement('div');
+      hider.className = 'wtHider';
+
+      if (parent) {
+        // if TABLE is detached (e.g. in Jasmine test), it has no parentNode so we cannot attach holder to it
+        parent.insertBefore(hider, spreader);
+      }
+      hider.appendChild(spreader);
+    }
+
+    return hider;
+  }
+
+  /**
+   *
+   * @param hider
+   * @returns {HTMLElement}
+   */
+  createHolder(hider) {
+    const parent = hider.parentNode;
+    let holder;
+
+    if (!parent || parent.nodeType !== 1 || !hasClass(parent, 'wtHolder')) {
+      holder = document.createElement('div');
+      holder.style.position = 'relative';
+      holder.className = 'wtHolder';
+
+      if (parent) {
+        // if TABLE is detached (e.g. in Jasmine test), it has no parentNode so we cannot attach holder to it
+        parent.insertBefore(holder, hider);
+      }
+      if (!this.isWorkingOnClone()) {
+        holder.parentNode.className += 'ht_master handsontable';
+      }
+      holder.appendChild(hider);
+    }
+
+    return holder;
+  }
+
+  alignOverlaysWithTrimmingContainer() {
+    const trimmingElement = getTrimmingContainer(this.wtRootElement);
+
+    if (!this.isWorkingOnClone()) {
+      this.holder.parentNode.style.position = 'relative';
+
+      if (trimmingElement === window) {
+        const preventOverflow = this.wot.getSetting('preventOverflow');
+
+        if (!preventOverflow) {
+          this.holder.style.overflow = 'visible';
+          this.wtRootElement.style.overflow = 'visible';
+        }
+      } else {
+        this.holder.style.width = getStyle(trimmingElement, 'width');
+        this.holder.style.height = getStyle(trimmingElement, 'height');
+        this.holder.style.overflow = '';
+      }
+    }
+  }
+
+  isWorkingOnClone() {
+    return !!this.wot.cloneSource;
+  }
+
+  /**
+   * Redraws the table
+   *
+   * @param {Boolean} fastDraw If TRUE, will try to avoid full redraw and only update the border positions. If FALSE or UNDEFINED, will perform a full redraw
+   * @returns {Table}
+   */
+  draw(fastDraw) {
+    const { wtOverlays, wtViewport } = this.wot;
+    const totalRows = this.instance.getSetting('totalRows');
+    const rowHeaders = this.wot.getSetting('rowHeaders').length;
+    const columnHeaders = this.wot.getSetting('columnHeaders').length;
+    let syncScroll = false;
+    let runFastDraw = fastDraw;
+
+    if (!this.isWorkingOnClone()) {
+      this.holderOffset = offset(this.holder);
+      runFastDraw = wtViewport.createRenderCalculators(runFastDraw);
+
+      if (rowHeaders && !this.wot.getSetting('fixedColumnsLeft')) {
+        const leftScrollPos = wtOverlays.leftOverlay.getScrollPosition();
+        const previousState = this.correctHeaderWidth;
+
+        this.correctHeaderWidth = leftScrollPos > 0;
+
+        if (previousState !== this.correctHeaderWidth) {
+          runFastDraw = false;
+        }
+      }
+    }
+
+    if (!this.isWorkingOnClone()) {
+      syncScroll = wtOverlays.prepareOverlays();
+    }
+
+    if (runFastDraw) {
+      if (!this.isWorkingOnClone()) {
+        // in case we only scrolled without redraw, update visible rows information in oldRowsCalculator
+        wtViewport.createVisibleCalculators();
+      }
+      if (wtOverlays) {
+        wtOverlays.refresh(true);
+      }
+    } else {
+      if (this.isWorkingOnClone()) {
+        this.tableOffset = this.wot.cloneSource.wtTable.tableOffset;
+      } else {
+        this.tableOffset = offset(this.TABLE);
+      }
+      let startRow;
+
+      if (Overlay.isOverlayTypeOf(this.wot.cloneOverlay, Overlay.CLONE_DEBUG) ||
+          Overlay.isOverlayTypeOf(this.wot.cloneOverlay, Overlay.CLONE_TOP) ||
+          Overlay.isOverlayTypeOf(this.wot.cloneOverlay, Overlay.CLONE_TOP_LEFT_CORNER)) {
+        startRow = 0;
+      } else if (Overlay.isOverlayTypeOf(this.instance.cloneOverlay, Overlay.CLONE_BOTTOM) ||
+          Overlay.isOverlayTypeOf(this.instance.cloneOverlay, Overlay.CLONE_BOTTOM_LEFT_CORNER)) {
+        startRow = Math.max(totalRows - this.wot.getSetting('fixedRowsBottom'), 0);
+      } else {
+        startRow = wtViewport.rowsRenderCalculator.startRow;
+      }
+      let startColumn;
+
+      if (Overlay.isOverlayTypeOf(this.wot.cloneOverlay, Overlay.CLONE_DEBUG) ||
+          Overlay.isOverlayTypeOf(this.wot.cloneOverlay, Overlay.CLONE_LEFT) ||
+          Overlay.isOverlayTypeOf(this.wot.cloneOverlay, Overlay.CLONE_TOP_LEFT_CORNER) ||
+          Overlay.isOverlayTypeOf(this.wot.cloneOverlay, Overlay.CLONE_BOTTOM_LEFT_CORNER)) {
+        startColumn = 0;
+      } else {
+        startColumn = wtViewport.columnsRenderCalculator.startColumn;
+      }
+      this.rowFilter = new RowFilter(startRow, totalRows, columnHeaders);
+      this.columnFilter = new ColumnFilter(startColumn, this.wot.getSetting('totalColumns'), rowHeaders);
+
+      this.alignOverlaysWithTrimmingContainer();
+      this._doDraw(); // creates calculator after draw
+    }
+    this.refreshSelections(runFastDraw);
+
+    if (!this.isWorkingOnClone()) {
+      wtOverlays.topOverlay.resetFixedPosition();
+
+      if (wtOverlays.bottomOverlay.clone) {
+        wtOverlays.bottomOverlay.resetFixedPosition();
+      }
+
+      wtOverlays.leftOverlay.resetFixedPosition();
+
+      if (wtOverlays.topLeftCornerOverlay) {
+        wtOverlays.topLeftCornerOverlay.resetFixedPosition();
+      }
+
+      if (wtOverlays.bottomLeftCornerOverlay && wtOverlays.bottomLeftCornerOverlay.clone) {
+        wtOverlays.bottomLeftCornerOverlay.resetFixedPosition();
+      }
+    }
+    if (syncScroll) {
+      wtOverlays.syncScrollWithMaster();
+    }
+    this.wot.drawn = true;
+
+    return this;
+  }
+
+  _doDraw() {
+    const wtRenderer = new TableRenderer(this);
+
+    wtRenderer.render();
+  }
+
+  removeClassFromCells(className) {
+    const nodes = this.TABLE.querySelectorAll(`.${className}`);
+
+    for (let i = 0, len = nodes.length; i < len; i++) {
+      removeClass(nodes[i], className);
+    }
+  }
+
+  /**
+   * Refresh the table selection by re-rendering Selection instances connected with that instance.
+   *
+   * @param {Boolean} fastDraw If fast drawing is enabled than additionally className clearing is applied.
+   */
+  refreshSelections(fastDraw) {
+    if (!this.wot.selections) {
+      return;
+    }
+    const highlights = Array.from(this.wot.selections);
+    const len = highlights.length;
+
+    if (fastDraw) {
+      const classesToRemove = [];
+
+      for (let i = 0; i < len; i++) {
+        const {
+          highlightHeaderClassName,
+          highlightRowClassName,
+          highlightColumnClassName,
+        } = highlights[i].settings;
+        const classNames = highlights[i].classNames;
+        const classNamesLength = classNames.length;
+
+        for (let j = 0; j < classNamesLength; j++) {
+          if (!classesToRemove.includes(classNames[j])) {
+            classesToRemove.push(classNames[j]);
+          }
+        }
+
+        if (highlightHeaderClassName && !classesToRemove.includes(highlightHeaderClassName)) {
+          classesToRemove.push(highlightHeaderClassName);
+        }
+        if (highlightRowClassName && !classesToRemove.includes(highlightRowClassName)) {
+          classesToRemove.push(highlightRowClassName);
+        }
+        if (highlightColumnClassName && !classesToRemove.includes(highlightColumnClassName)) {
+          classesToRemove.push(highlightColumnClassName);
+        }
+      }
+
+      const additionalClassesToRemove = this.wot.getSetting('onBeforeRemoveCellClassNames');
+
+      if (Array.isArray(additionalClassesToRemove)) {
+        for (let i = 0; i < additionalClassesToRemove.length; i++) {
+          classesToRemove.push(additionalClassesToRemove[i]);
+        }
+      }
+
+      const classesToRemoveLength = classesToRemove.length;
+
+      for (let i = 0; i < classesToRemoveLength; i++) {
+        // there was no rerender, so we need to remove classNames by ourselves
+        this.removeClassFromCells(classesToRemove[i]);
+      }
+    }
+
+    for (let i = 0; i < len; i++) {
+      highlights[i].draw(this.wot, fastDraw);
+    }
+  }
+
+  /**
+   * Get cell element at coords.
+   *
+   * @param {CellCoords} coords
+   * @returns {HTMLElement|Number} HTMLElement on success or Number one of the exit codes on error:
+   *  -1 row before viewport
+   *  -2 row after viewport
+   */
+  getCell(coords) {
+    let row = coords.row;
+    let column = coords.col;
+    const hookResult = this.wot.getSetting('onModifyGetCellCoords', row, column);
+
+    if (hookResult && Array.isArray(hookResult)) {
+      [row, column] = hookResult;
+    }
+
+    if (this.isRowBeforeRenderedRows(row)) {
+      // row before rendered rows
+      return -1;
+
+    } else if (this.isRowAfterRenderedRows(row)) {
+      // row after rendered rows
+      return -2;
+    }
+
+    const TR = this.TBODY.childNodes[this.rowFilter.sourceToRendered(row)];
+
+    if (TR) {
+      return TR.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(column)];
+    }
+  }
+
+  /**
+   * getColumnHeader
+   *
+   * @param {Number} col Column index
+   * @param {Number} [level=0] Header level (0 = most distant to the table)
+   * @returns {Object} HTMLElement on success or undefined on error
+   */
+  getColumnHeader(col, level = 0) {
+    const TR = this.THEAD.childNodes[level];
+
+    if (TR) {
+      return TR.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(col)];
+    }
+  }
+
+  /**
+   * getRowHeader
+   *
+   * @param {Number} row Row index
+   * @returns {HTMLElement} HTMLElement on success or Number one of the exit codes on error: `null table doesn't have row headers`
+   */
+  getRowHeader(row) {
+    if (this.columnFilter.sourceColumnToVisibleRowHeadedColumn(0) === 0) {
+      return null;
+    }
+    const TR = this.TBODY.childNodes[this.rowFilter.sourceToRendered(row)];
+
+    if (TR) {
+      return TR.childNodes[0];
+    }
+  }
+
+  /**
+   * Returns cell coords object for a given TD (or a child element of a TD element).
+   *
+   * @param {HTMLTableCellElement} TD A cell DOM element (or a child of one).
+   * @returns {CellCoords|null} The coordinates of the provided TD element (or the closest TD element) or null, if the provided element is not applicable.
+   */
+  getCoords(TD) {
+    let cellElement = TD;
+
+    if (cellElement.nodeName !== 'TD' && cellElement.nodeName !== 'TH') {
+      cellElement = closest(cellElement, ['TD', 'TH']);
+    }
+
+    if (cellElement === null) {
+      return null;
+    }
+
+    const TR = cellElement.parentNode;
+    const CONTAINER = TR.parentNode;
+    let row = index(TR);
+    let col = cellElement.cellIndex;
+
+    if (overlayContainsElement(Overlay.CLONE_TOP_LEFT_CORNER, cellElement) || overlayContainsElement(Overlay.CLONE_TOP, cellElement)) {
+      if (CONTAINER.nodeName === 'THEAD') {
+        row -= CONTAINER.childNodes.length;
+      }
+
+    } else if (CONTAINER === this.THEAD) {
+      row = this.rowFilter.visibleColHeadedRowToSourceRow(row);
+
+    } else {
+      row = this.rowFilter.renderedToSource(row);
+    }
+
+    if (overlayContainsElement(Overlay.CLONE_TOP_LEFT_CORNER, cellElement) || overlayContainsElement(Overlay.CLONE_LEFT, cellElement)) {
+      col = this.columnFilter.offsettedTH(col);
+
+    } else {
+      col = this.columnFilter.visibleRowHeadedColumnToSourceColumn(col);
+    }
+
+    return new CellCoords(row, col);
+  }
+
+  getTrForRow(row) {
+    return this.TBODY.childNodes[this.rowFilter.sourceToRendered(row)];
+  }
+
+  getFirstRenderedRow() {
+    return this.wot.wtViewport.rowsRenderCalculator.startRow;
+  }
+
+  getFirstVisibleRow() {
+    return this.wot.wtViewport.rowsVisibleCalculator.startRow;
+  }
+
+  getFirstRenderedColumn() {
+    return this.wot.wtViewport.columnsRenderCalculator.startColumn;
+  }
+
+  /**
+   * @returns {Number} Returns -1 if no row is visible
+   */
+  getFirstVisibleColumn() {
+    return this.wot.wtViewport.columnsVisibleCalculator.startColumn;
+  }
+
+  /**
+   * @returns {Number} Returns -1 if no row is visible
+   */
+  getLastRenderedRow() {
+    return this.wot.wtViewport.rowsRenderCalculator.endRow;
+  }
+
+  getLastVisibleRow() {
+    return this.wot.wtViewport.rowsVisibleCalculator.endRow;
+  }
+
+  getLastRenderedColumn() {
+    return this.wot.wtViewport.columnsRenderCalculator.endColumn;
+  }
+
+  /**
+   * @returns {Number} Returns -1 if no column is visible
+   */
+  getLastVisibleColumn() {
+    return this.wot.wtViewport.columnsVisibleCalculator.endColumn;
+  }
+
+  isRowBeforeRenderedRows(row) {
+    return this.rowFilter && (this.rowFilter.sourceToRendered(row) < 0 && row >= 0);
+  }
+
+  isRowAfterViewport(row) {
+    return this.rowFilter && (this.rowFilter.sourceToRendered(row) > this.getLastVisibleRow());
+  }
+
+  isRowAfterRenderedRows(row) {
+    return this.rowFilter && (this.rowFilter.sourceToRendered(row) > this.getLastRenderedRow());
+  }
+
+  isColumnBeforeViewport(column) {
+    return this.columnFilter && (this.columnFilter.sourceToRendered(column) < 0 && column >= 0);
+  }
+
+  isColumnAfterViewport(column) {
+    return this.columnFilter && (this.columnFilter.sourceToRendered(column) > this.getLastVisibleColumn());
+  }
+
+  isLastRowFullyVisible() {
+    return this.getLastVisibleRow() === this.getLastRenderedRow();
+  }
+
+  isLastColumnFullyVisible() {
+    return this.getLastVisibleColumn() === this.getLastRenderedColumn();
+  }
+
+  getRenderedColumnsCount() {
+    let columnsCount = this.wot.wtViewport.columnsRenderCalculator.count;
+    const totalColumns = this.wot.getSetting('totalColumns');
+
+    if (this.wot.isOverlayName(Overlay.CLONE_DEBUG)) {
+      columnsCount = totalColumns;
+
+    } else if (this.wot.isOverlayName(Overlay.CLONE_LEFT) ||
+               this.wot.isOverlayName(Overlay.CLONE_TOP_LEFT_CORNER) ||
+               this.wot.isOverlayName(Overlay.CLONE_BOTTOM_LEFT_CORNER)) {
+      return Math.min(this.wot.getSetting('fixedColumnsLeft'), totalColumns);
+
+    }
+
+    return columnsCount;
+  }
+
+  getRenderedRowsCount() {
+    let rowsCount = this.wot.wtViewport.rowsRenderCalculator.count;
+    const totalRows = this.wot.getSetting('totalRows');
+
+    if (this.wot.isOverlayName(Overlay.CLONE_DEBUG)) {
+      rowsCount = totalRows;
+
+    } else if (this.wot.isOverlayName(Overlay.CLONE_TOP) ||
+               this.wot.isOverlayName(Overlay.CLONE_TOP_LEFT_CORNER)) {
+      rowsCount = Math.min(this.wot.getSetting('fixedRowsTop'), totalRows);
+
+    } else if (this.wot.isOverlayName(Overlay.CLONE_BOTTOM) ||
+               this.wot.isOverlayName(Overlay.CLONE_BOTTOM_LEFT_CORNER)) {
+      rowsCount = Math.min(this.wot.getSetting('fixedRowsBottom'), totalRows);
+    }
+
+    return rowsCount;
+  }
+
+  getVisibleRowsCount() {
+    return this.wot.wtViewport.rowsVisibleCalculator.count;
+  }
+
+  allRowsInViewport() {
+    return this.wot.getSetting('totalRows') === this.getVisibleRowsCount();
+  }
+
+  /**
+   * Checks if any of the row's cells content exceeds its initial height, and if so, returns the oversized height
+   *
+   * @param {Number} sourceRow
+   * @returns {Number}
+   */
+  getRowHeight(sourceRow) {
+    let height = this.wot.wtSettings.settings.rowHeight(sourceRow);
+    const oversizedHeight = this.wot.wtViewport.oversizedRows[sourceRow];
+
+    if (oversizedHeight !== void 0) {
+      height = height === void 0 ? oversizedHeight : Math.max(height, oversizedHeight);
+    }
+
+    return height;
+  }
+
+  getColumnHeaderHeight(level) {
+    let height = this.wot.wtSettings.settings.defaultRowHeight;
+    const oversizedHeight = this.wot.wtViewport.oversizedColumnHeaders[level];
+
+    if (oversizedHeight !== void 0) {
+      height = height ? Math.max(height, oversizedHeight) : oversizedHeight;
+    }
+
+    return height;
+  }
+
+  getVisibleColumnsCount() {
+    return this.wot.wtViewport.columnsVisibleCalculator.count;
+  }
+
+  allColumnsInViewport() {
+    return this.wot.getSetting('totalColumns') === this.getVisibleColumnsCount();
+  }
+
+  getColumnWidth(sourceColumn) {
+    let width = this.wot.wtSettings.settings.columnWidth;
+
+    if (typeof width === 'function') {
+      width = width(sourceColumn);
+
+    } else if (typeof width === 'object') {
+      width = width[sourceColumn];
+    }
+
+    return width || this.wot.wtSettings.settings.defaultColumnWidth;
+  }
+
+  getStretchedColumnWidth(sourceColumn) {
+    const columnWidth = this.getColumnWidth(sourceColumn);
+    let width = (columnWidth === null || columnWidth === void 0) ? this.instance.wtSettings.settings.defaultColumnWidth : columnWidth;
+    const calculator = this.wot.wtViewport.columnsRenderCalculator;
+
+    if (calculator) {
+      const stretchedWidth = calculator.getStretchedColumnWidth(sourceColumn, width);
+
+      if (stretchedWidth) {
+        width = stretchedWidth;
+      }
+    }
+
+    return width;
+  }
+
+  /**
+   * Modify row header widths provided by user in class contructor.
+   *
+   * @private
+   */
+  _modifyRowHeaderWidth(rowHeaderWidthFactory) {
+    let widths = isFunction(rowHeaderWidthFactory) ? rowHeaderWidthFactory() : null;
+
+    if (Array.isArray(widths)) {
+      widths = [...widths];
+      widths[widths.length - 1] = this._correctRowHeaderWidth(widths[widths.length - 1]);
+    } else {
+      widths = this._correctRowHeaderWidth(widths);
+    }
+
+    return widths;
+  }
+
+  /**
+   * Correct row header width if necessary.
+   *
+   * @private
+   */
+  _correctRowHeaderWidth(width) {
+    let rowHeaderWidth = width;
+
+    if (typeof width !== 'number') {
+      rowHeaderWidth = this.wot.getSetting('defaultColumnWidth');
+    }
+    if (this.correctHeaderWidth) {
+      rowHeaderWidth += 1;
+    }
+
+    return rowHeaderWidth;
+  }
+}
+
+export default Table;

+ 662 - 0
handsontable/src/3rdparty/walkontable/src/tableRenderer.js

@@ -0,0 +1,662 @@
+import {
+  addClass,
+  empty,
+  getScrollbarWidth,
+  hasClass,
+  innerHeight,
+  outerWidth
+} from './../../../helpers/dom/element';
+import { warn } from './../../../helpers/console';
+import { toSingleLine } from './../../../helpers/templateLiteralTag';
+import Overlay from './overlay/_base';
+
+let performanceWarningAppeared = false;
+
+/**
+ * @class TableRenderer
+ */
+class TableRenderer {
+  /**
+   * @param {WalkontableTable} wtTable
+   */
+  constructor(wtTable) {
+    this.wtTable = wtTable;
+    this.wot = wtTable.instance;
+
+    // legacy support
+    this.instance = wtTable.instance;
+
+    this.rowFilter = wtTable.rowFilter;
+    this.columnFilter = wtTable.columnFilter;
+
+    this.TABLE = wtTable.TABLE;
+    this.THEAD = wtTable.THEAD;
+    this.TBODY = wtTable.TBODY;
+    this.COLGROUP = wtTable.COLGROUP;
+
+    this.rowHeaders = [];
+    this.rowHeaderCount = 0;
+    this.columnHeaders = [];
+    this.columnHeaderCount = 0;
+    this.fixedRowsTop = 0;
+    this.fixedRowsBottom = 0;
+  }
+
+  /**
+   *
+   */
+  render() {
+    if (!this.wtTable.isWorkingOnClone()) {
+      const skipRender = {};
+      this.wot.getSetting('beforeDraw', true, skipRender);
+
+      if (skipRender.skipRender === true) {
+        return;
+      }
+    }
+
+    this.rowHeaders = this.wot.getSetting('rowHeaders');
+    this.rowHeaderCount = this.rowHeaders.length;
+    this.fixedRowsTop = this.wot.getSetting('fixedRowsTop');
+    this.fixedRowsBottom = this.wot.getSetting('fixedRowsBottom');
+    this.columnHeaders = this.wot.getSetting('columnHeaders');
+    this.columnHeaderCount = this.columnHeaders.length;
+
+    const columnsToRender = this.wtTable.getRenderedColumnsCount();
+    const rowsToRender = this.wtTable.getRenderedRowsCount();
+    const totalColumns = this.wot.getSetting('totalColumns');
+    const totalRows = this.wot.getSetting('totalRows');
+    let workspaceWidth;
+    let adjusted = false;
+
+    if (Overlay.isOverlayTypeOf(this.wot.cloneOverlay, Overlay.CLONE_BOTTOM) ||
+      Overlay.isOverlayTypeOf(this.wot.cloneOverlay, Overlay.CLONE_BOTTOM_LEFT_CORNER)) {
+
+      // do NOT render headers on the bottom or bottom-left corner overlay
+      this.columnHeaders = [];
+      this.columnHeaderCount = 0;
+    }
+
+    if (totalColumns >= 0) {
+      // prepare COL and TH elements for rendering
+      this.adjustAvailableNodes();
+      adjusted = true;
+
+      // adjust column widths according to user widths settings
+      this.renderColumnHeaders();
+
+      // Render table rows
+      this.renderRows(totalRows, rowsToRender, columnsToRender);
+
+      if (!this.wtTable.isWorkingOnClone()) {
+        workspaceWidth = this.wot.wtViewport.getWorkspaceWidth();
+        this.wot.wtViewport.containerWidth = null;
+      }
+
+      this.adjustColumnWidths(columnsToRender);
+      this.markOversizedColumnHeaders();
+      this.adjustColumnHeaderHeights();
+    }
+
+    if (!adjusted) {
+      this.adjustAvailableNodes();
+    }
+    this.removeRedundantRows(rowsToRender);
+
+    if (!this.wtTable.isWorkingOnClone() || this.wot.isOverlayName(Overlay.CLONE_BOTTOM)) {
+      this.markOversizedRows();
+    }
+    if (!this.wtTable.isWorkingOnClone()) {
+      this.wot.wtViewport.createVisibleCalculators();
+      this.wot.wtOverlays.refresh(false);
+
+      this.wot.wtOverlays.applyToDOM();
+
+      const hiderWidth = outerWidth(this.wtTable.hider);
+      const tableWidth = outerWidth(this.wtTable.TABLE);
+
+      if (hiderWidth !== 0 && (tableWidth !== hiderWidth)) {
+        // Recalculate the column widths, if width changes made in the overlays removed the scrollbar, thus changing the viewport width.
+        this.adjustColumnWidths(columnsToRender);
+      }
+
+      if (workspaceWidth !== this.wot.wtViewport.getWorkspaceWidth()) {
+        // workspace width changed though to shown/hidden vertical scrollbar. Let's reapply stretching
+        this.wot.wtViewport.containerWidth = null;
+
+        const firstRendered = this.wtTable.getFirstRenderedColumn();
+        const lastRendered = this.wtTable.getLastRenderedColumn();
+        const defaultColumnWidth = this.wot.getSetting('defaultColumnWidth');
+        let rowHeaderWidthSetting = this.wot.getSetting('rowHeaderWidth');
+
+        rowHeaderWidthSetting = this.instance.getSetting('onModifyRowHeaderWidth', rowHeaderWidthSetting);
+
+        if (rowHeaderWidthSetting !== null && rowHeaderWidthSetting !== void 0) {
+          for (let i = 0; i < this.rowHeaderCount; i++) {
+            let width = Array.isArray(rowHeaderWidthSetting) ? rowHeaderWidthSetting[i] : rowHeaderWidthSetting;
+
+            width = (width === null || width === void 0) ? defaultColumnWidth : width;
+
+            this.COLGROUP.childNodes[i].style.width = `${width}px`;
+          }
+        }
+
+        for (let i = firstRendered; i < lastRendered; i++) {
+          const width = this.wtTable.getStretchedColumnWidth(i);
+          const renderedIndex = this.columnFilter.sourceToRendered(i);
+
+          this.COLGROUP.childNodes[renderedIndex + this.rowHeaderCount].style.width = `${width}px`;
+        }
+      }
+
+      this.wot.getSetting('onDraw', true);
+
+    } else if (this.wot.isOverlayName(Overlay.CLONE_BOTTOM)) {
+      this.wot.cloneSource.wtOverlays.adjustElementsSize();
+    }
+  }
+
+  /**
+   * @param {Number} renderedRowsCount
+   */
+  removeRedundantRows(renderedRowsCount) {
+    while (this.wtTable.tbodyChildrenLength > renderedRowsCount) {
+      this.TBODY.removeChild(this.TBODY.lastChild);
+      this.wtTable.tbodyChildrenLength -= 1;
+    }
+  }
+
+  /**
+   * @param {Number} totalRows
+   * @param {Number} rowsToRender
+   * @param {Number} columnsToRender
+   */
+  renderRows(totalRows, rowsToRender, columnsToRender) {
+    let TR;
+    let visibleRowIndex = 0;
+    let sourceRowIndex = this.rowFilter.renderedToSource(visibleRowIndex);
+    const isWorkingOnClone = this.wtTable.isWorkingOnClone();
+
+    while (sourceRowIndex < totalRows && sourceRowIndex >= 0) {
+      if (!performanceWarningAppeared && visibleRowIndex > 1000) {
+        performanceWarningAppeared = true;
+        warn(toSingleLine`Performance tip: Handsontable rendered more than 1000 visible rows. Consider limiting the number 
+          of rendered rows by specifying the table height and/or turning off the "renderAllRows" option.`);
+      }
+      if (rowsToRender !== void 0 && visibleRowIndex === rowsToRender) {
+        // We have as much rows as needed for this clone
+        break;
+      }
+      TR = this.getOrCreateTrForRow(visibleRowIndex, TR);
+
+      // Render row headers
+      this.renderRowHeaders(sourceRowIndex, TR);
+      // Add and/or remove TDs to TR to match the desired number
+      this.adjustColumns(TR, columnsToRender + this.rowHeaderCount);
+      // Render cells
+      this.renderCells(sourceRowIndex, TR, columnsToRender);
+
+      if (!isWorkingOnClone ||
+          // Necessary to refresh oversized row heights after editing cell in overlays
+          this.wot.isOverlayName(Overlay.CLONE_BOTTOM)) {
+        // Reset the oversized row cache for this row
+        this.resetOversizedRow(sourceRowIndex);
+      }
+
+      if (TR.firstChild) {
+        // if I have 2 fixed columns with one-line content and the 3rd column has a multiline content, this is
+        // the way to make sure that the overlay will has same row height
+        let height = this.wot.wtTable.getRowHeight(sourceRowIndex);
+
+        if (height) {
+          // Decrease height. 1 pixel will be "replaced" by 1px border top
+          height -= 1;
+          TR.firstChild.style.height = `${height}px`;
+        } else {
+          TR.firstChild.style.height = '';
+        }
+      }
+      visibleRowIndex += 1;
+      sourceRowIndex = this.rowFilter.renderedToSource(visibleRowIndex);
+    }
+  }
+
+  /**
+   * Reset the oversized row cache for the provided index
+   *
+   * @param {Number} sourceRow Row index
+   */
+  resetOversizedRow(sourceRow) {
+    if (this.wot.getSetting('externalRowCalculator')) {
+      return;
+    }
+    if (this.wot.wtViewport.oversizedRows && this.wot.wtViewport.oversizedRows[sourceRow]) {
+      this.wot.wtViewport.oversizedRows[sourceRow] = void 0;
+    }
+  }
+
+  /**
+   * Check if any of the rendered rows is higher than expected, and if so, cache them
+   */
+  markOversizedRows() {
+    if (this.wot.getSetting('externalRowCalculator')) {
+      return;
+    }
+    let rowCount = this.instance.wtTable.TBODY.childNodes.length;
+    const expectedTableHeight = rowCount * this.instance.wtSettings.settings.defaultRowHeight;
+    const actualTableHeight = innerHeight(this.instance.wtTable.TBODY) - 1;
+    let previousRowHeight;
+    let rowInnerHeight;
+    let sourceRowIndex;
+    let currentTr;
+    let rowHeader;
+
+    if (expectedTableHeight === actualTableHeight && !this.instance.getSetting('fixedRowsBottom')) {
+      // If the actual table height equals rowCount * default single row height, no row is oversized -> no need to iterate over them
+      return;
+    }
+
+    while (rowCount) {
+      rowCount -= 1;
+      sourceRowIndex = this.instance.wtTable.rowFilter.renderedToSource(rowCount);
+      previousRowHeight = this.instance.wtTable.getRowHeight(sourceRowIndex);
+      currentTr = this.instance.wtTable.getTrForRow(sourceRowIndex);
+      rowHeader = currentTr.querySelector('th');
+
+      if (rowHeader) {
+        rowInnerHeight = innerHeight(rowHeader);
+      } else {
+        rowInnerHeight = innerHeight(currentTr) - 1;
+      }
+
+      if ((!previousRowHeight && this.instance.wtSettings.settings.defaultRowHeight < rowInnerHeight ||
+          previousRowHeight < rowInnerHeight)) {
+        rowInnerHeight += 1;
+        this.instance.wtViewport.oversizedRows[sourceRowIndex] = rowInnerHeight;
+      }
+    }
+  }
+
+  /**
+   * Check if any of the rendered columns is higher than expected, and if so, cache them.
+   */
+  markOversizedColumnHeaders() {
+    const overlayName = this.wot.getOverlayName();
+
+    if (!this.columnHeaderCount || this.wot.wtViewport.hasOversizedColumnHeadersMarked[overlayName] || this.wtTable.isWorkingOnClone()) {
+      return;
+    }
+    const columnCount = this.wtTable.getRenderedColumnsCount();
+
+    for (let i = 0; i < this.columnHeaderCount; i++) {
+      for (let renderedColumnIndex = (-1) * this.rowHeaderCount; renderedColumnIndex < columnCount; renderedColumnIndex++) {
+        this.markIfOversizedColumnHeader(renderedColumnIndex);
+      }
+    }
+    this.wot.wtViewport.hasOversizedColumnHeadersMarked[overlayName] = true;
+  }
+
+  /**
+   *
+   */
+  adjustColumnHeaderHeights() {
+    const columnHeaders = this.wot.getSetting('columnHeaders');
+    const children = this.wot.wtTable.THEAD.childNodes;
+    const oversizedColumnHeaders = this.wot.wtViewport.oversizedColumnHeaders;
+
+    for (let i = 0, len = columnHeaders.length; i < len; i++) {
+      if (oversizedColumnHeaders[i]) {
+        if (!children[i] || children[i].childNodes.length === 0) {
+          return;
+        }
+        children[i].childNodes[0].style.height = `${oversizedColumnHeaders[i]}px`;
+      }
+    }
+  }
+
+  /**
+   * Check if column header for the specified column is higher than expected, and if so, cache it
+   *
+   * @param {Number} col Index of column
+   */
+  markIfOversizedColumnHeader(col) {
+    const sourceColIndex = this.wot.wtTable.columnFilter.renderedToSource(col);
+    let level = this.columnHeaderCount;
+    const defaultRowHeight = this.wot.wtSettings.settings.defaultRowHeight;
+    let previousColHeaderHeight;
+    let currentHeader;
+    let currentHeaderHeight;
+    const columnHeaderHeightSetting = this.wot.getSetting('columnHeaderHeight') || [];
+
+    while (level) {
+      level -= 1;
+
+      previousColHeaderHeight = this.wot.wtTable.getColumnHeaderHeight(level);
+      currentHeader = this.wot.wtTable.getColumnHeader(sourceColIndex, level);
+
+      if (!currentHeader) {
+        /* eslint-disable no-continue */
+        continue;
+      }
+      currentHeaderHeight = innerHeight(currentHeader);
+
+      if (!previousColHeaderHeight && defaultRowHeight < currentHeaderHeight || previousColHeaderHeight < currentHeaderHeight) {
+        this.wot.wtViewport.oversizedColumnHeaders[level] = currentHeaderHeight;
+      }
+
+      if (Array.isArray(columnHeaderHeightSetting)) {
+        if (columnHeaderHeightSetting[level] !== null && columnHeaderHeightSetting[level] !== void 0) {
+          this.wot.wtViewport.oversizedColumnHeaders[level] = columnHeaderHeightSetting[level];
+        }
+
+      } else if (!isNaN(columnHeaderHeightSetting)) {
+        this.wot.wtViewport.oversizedColumnHeaders[level] = columnHeaderHeightSetting;
+      }
+
+      if (this.wot.wtViewport.oversizedColumnHeaders[level] < (columnHeaderHeightSetting[level] || columnHeaderHeightSetting)) {
+        this.wot.wtViewport.oversizedColumnHeaders[level] = (columnHeaderHeightSetting[level] || columnHeaderHeightSetting);
+      }
+    }
+  }
+
+  /**
+   * @param {Number} sourceRowIndex
+   * @param {HTMLTableRowElement} TR
+   * @param {Number} columnsToRender
+   * @returns {HTMLTableCellElement}
+   */
+  renderCells(sourceRowIndex, TR, columnsToRender) {
+    let TD;
+    let sourceColIndex;
+
+    for (let visibleColIndex = 0; visibleColIndex < columnsToRender; visibleColIndex++) {
+      sourceColIndex = this.columnFilter.renderedToSource(visibleColIndex);
+
+      if (visibleColIndex === 0) {
+        TD = TR.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(sourceColIndex)];
+      } else {
+        TD = TD.nextSibling; // http://jsperf.com/nextsibling-vs-indexed-childnodes
+      }
+      // If the number of headers has been reduced, we need to replace excess TH with TD
+      if (TD.nodeName === 'TH') {
+        TD = replaceThWithTd(TD, TR);
+      }
+      if (!hasClass(TD, 'hide')) {
+        TD.className = '';
+      }
+      TD.removeAttribute('style');
+      this.wot.wtSettings.settings.cellRenderer(sourceRowIndex, sourceColIndex, TD);
+    }
+
+    return TD;
+  }
+
+  /**
+   * @param {Number} columnsToRender Number of columns to render.
+   */
+  adjustColumnWidths(columnsToRender) {
+    let scrollbarCompensation = 0;
+    const sourceInstance = this.wot.cloneSource ? this.wot.cloneSource : this.wot;
+    const mainHolder = sourceInstance.wtTable.holder;
+    const defaultColumnWidth = this.wot.getSetting('defaultColumnWidth');
+    let rowHeaderWidthSetting = this.wot.getSetting('rowHeaderWidth');
+
+    if (mainHolder.offsetHeight < mainHolder.scrollHeight) {
+      scrollbarCompensation = getScrollbarWidth();
+    }
+    this.wot.wtViewport.columnsRenderCalculator.refreshStretching(this.wot.wtViewport.getViewportWidth() - scrollbarCompensation);
+
+    rowHeaderWidthSetting = this.instance.getSetting('onModifyRowHeaderWidth', rowHeaderWidthSetting);
+
+    if (rowHeaderWidthSetting !== null && rowHeaderWidthSetting !== void 0) {
+      for (let i = 0; i < this.rowHeaderCount; i++) {
+        let width = Array.isArray(rowHeaderWidthSetting) ? rowHeaderWidthSetting[i] : rowHeaderWidthSetting;
+
+        width = (width === null || width === void 0) ? defaultColumnWidth : width;
+
+        this.COLGROUP.childNodes[i].style.width = `${width}px`;
+      }
+    }
+
+    for (let renderedColIndex = 0; renderedColIndex < columnsToRender; renderedColIndex++) {
+      const width = this.wtTable.getStretchedColumnWidth(this.columnFilter.renderedToSource(renderedColIndex));
+
+      this.COLGROUP.childNodes[renderedColIndex + this.rowHeaderCount].style.width = `${width}px`;
+    }
+  }
+
+  /**
+   * @param {HTMLTableCellElement} TR
+   */
+  appendToTbody(TR) {
+    this.TBODY.appendChild(TR);
+    this.wtTable.tbodyChildrenLength += 1;
+  }
+
+  /**
+   * @param {Number} rowIndex
+   * @param {HTMLTableRowElement} currentTr
+   * @returns {HTMLTableCellElement}
+   */
+  getOrCreateTrForRow(rowIndex, currentTr) {
+    let TR;
+
+    if (rowIndex >= this.wtTable.tbodyChildrenLength) {
+      TR = this.createRow();
+      this.appendToTbody(TR);
+
+    } else if (rowIndex === 0) {
+      TR = this.TBODY.firstChild;
+
+    } else {
+      // http://jsperf.com/nextsibling-vs-indexed-childnodes
+      TR = currentTr.nextSibling;
+    }
+    if (TR.className) {
+      TR.removeAttribute('class');
+    }
+
+    return TR;
+  }
+
+  /**
+   * @returns {HTMLTableCellElement}
+   */
+  createRow() {
+    const TR = document.createElement('TR');
+
+    for (let visibleColIndex = 0; visibleColIndex < this.rowHeaderCount; visibleColIndex++) {
+      TR.appendChild(document.createElement('TH'));
+    }
+
+    return TR;
+  }
+
+  /**
+   * @param {Number} row
+   * @param {Number} col
+   * @param {HTMLTableCellElement} TH
+   */
+  renderRowHeader(row, col, TH) {
+    TH.className = '';
+    TH.removeAttribute('style');
+    this.rowHeaders[col](row, TH, col);
+  }
+
+  /**
+   * @param {Number} row
+   * @param {HTMLTableCellElement} TR
+   */
+  renderRowHeaders(row, TR) {
+    for (let TH = TR.firstChild, visibleColIndex = 0; visibleColIndex < this.rowHeaderCount; visibleColIndex++) {
+      // If the number of row headers increased we need to create TH or replace an existing TD node with TH
+      if (!TH) {
+        TH = document.createElement('TH');
+        TR.appendChild(TH);
+
+      } else if (TH.nodeName === 'TD') {
+        TH = replaceTdWithTh(TH, TR);
+      }
+      this.renderRowHeader(row, visibleColIndex, TH);
+      // http://jsperf.com/nextsibling-vs-indexed-childnodes
+      TH = TH.nextSibling;
+    }
+  }
+
+  /**
+   * Adjust the number of COL and TH elements to match the number of columns and headers that need to be rendered
+   */
+  adjustAvailableNodes() {
+    this.adjustColGroups();
+    this.adjustThead();
+  }
+
+  /**
+   * Renders the column headers
+   */
+  renderColumnHeaders() {
+    if (!this.columnHeaderCount) {
+      return;
+    }
+    const columnCount = this.wtTable.getRenderedColumnsCount();
+
+    for (let i = 0; i < this.columnHeaderCount; i++) {
+      const TR = this.getTrForColumnHeaders(i);
+
+      for (let renderedColumnIndex = (-1) * this.rowHeaderCount; renderedColumnIndex < columnCount; renderedColumnIndex++) {
+        const sourceCol = this.columnFilter.renderedToSource(renderedColumnIndex);
+
+        this.renderColumnHeader(i, sourceCol, TR.childNodes[renderedColumnIndex + this.rowHeaderCount]);
+      }
+    }
+  }
+
+  /**
+   * Adjusts the number of COL elements to match the number of columns that need to be rendered
+   */
+  adjustColGroups() {
+    const columnCount = this.wtTable.getRenderedColumnsCount();
+
+    while (this.wtTable.colgroupChildrenLength < columnCount + this.rowHeaderCount) {
+      this.COLGROUP.appendChild(document.createElement('COL'));
+      this.wtTable.colgroupChildrenLength += 1;
+    }
+    while (this.wtTable.colgroupChildrenLength > columnCount + this.rowHeaderCount) {
+      this.COLGROUP.removeChild(this.COLGROUP.lastChild);
+      this.wtTable.colgroupChildrenLength -= 1;
+    }
+    if (this.rowHeaderCount) {
+      addClass(this.COLGROUP.childNodes[0], 'rowHeader');
+    }
+  }
+
+  /**
+   * Adjusts the number of TH elements in THEAD to match the number of headers and columns that need to be rendered
+   */
+  adjustThead() {
+    const columnCount = this.wtTable.getRenderedColumnsCount();
+    let TR = this.THEAD.firstChild;
+
+    if (this.columnHeaders.length) {
+      for (let i = 0, len = this.columnHeaders.length; i < len; i++) {
+        TR = this.THEAD.childNodes[i];
+
+        if (!TR) {
+          TR = document.createElement('TR');
+          this.THEAD.appendChild(TR);
+        }
+        this.theadChildrenLength = TR.childNodes.length;
+
+        while (this.theadChildrenLength < columnCount + this.rowHeaderCount) {
+          TR.appendChild(document.createElement('TH'));
+          this.theadChildrenLength += 1;
+        }
+        while (this.theadChildrenLength > columnCount + this.rowHeaderCount) {
+          TR.removeChild(TR.lastChild);
+          this.theadChildrenLength -= 1;
+        }
+      }
+      const theadChildrenLength = this.THEAD.childNodes.length;
+
+      if (theadChildrenLength > this.columnHeaders.length) {
+        for (let i = this.columnHeaders.length; i < theadChildrenLength; i++) {
+          this.THEAD.removeChild(this.THEAD.lastChild);
+        }
+      }
+    } else if (TR) {
+      empty(TR);
+    }
+  }
+
+  /**
+   * @param {Number} index
+   * @returns {HTMLTableCellElement}
+   */
+  getTrForColumnHeaders(index) {
+    return this.THEAD.childNodes[index];
+  }
+
+  /**
+   * @param {Number} row
+   * @param {Number} col
+   * @param {HTMLTableCellElement} TH
+   * @returns {*}
+   */
+  renderColumnHeader(row, col, TH) {
+    TH.className = '';
+    TH.removeAttribute('style');
+
+    return this.columnHeaders[row](col, TH, row);
+  }
+
+  /**
+   * Add and/or remove the TDs to match the desired number
+   *
+   * @param {HTMLTableCellElement} TR Table row in question
+   * @param {Number} desiredCount The desired number of TDs in the TR
+   */
+  adjustColumns(TR, desiredCount) {
+    let count = TR.childNodes.length;
+
+    while (count < desiredCount) {
+      const TD = document.createElement('TD');
+
+      TR.appendChild(TD);
+      count += 1;
+    }
+    while (count > desiredCount) {
+      TR.removeChild(TR.lastChild);
+      count -= 1;
+    }
+  }
+
+  /**
+   * @param {Number} columnsToRender
+   */
+  removeRedundantColumns(columnsToRender) {
+    while (this.wtTable.tbodyChildrenLength > columnsToRender) {
+      this.TBODY.removeChild(this.TBODY.lastChild);
+      this.wtTable.tbodyChildrenLength -= 1;
+    }
+  }
+}
+
+function replaceTdWithTh(TD, TR) {
+  const TH = document.createElement('TH');
+
+  TR.insertBefore(TH, TD);
+  TR.removeChild(TD);
+
+  return TH;
+}
+
+function replaceThWithTd(TH, TR) {
+  const TD = document.createElement('TD');
+
+  TR.insertBefore(TD, TH);
+  TR.removeChild(TH);
+
+  return TD;
+}
+
+export default TableRenderer;

+ 489 - 0
handsontable/src/3rdparty/walkontable/src/viewport.js

@@ -0,0 +1,489 @@
+import {
+  getScrollbarWidth,
+  getStyle,
+  offset,
+  outerHeight,
+  outerWidth,
+} from './../../../helpers/dom/element';
+import { objectEach } from './../../../helpers/object';
+import EventManager from './../../../eventManager';
+import ViewportColumnsCalculator from './calculator/viewportColumns';
+import ViewportRowsCalculator from './calculator/viewportRows';
+
+/**
+ * @class Viewport
+ */
+class Viewport {
+  /**
+   * @param wotInstance
+   */
+  constructor(wotInstance) {
+    this.wot = wotInstance;
+    // legacy support
+    this.instance = this.wot;
+
+    this.oversizedRows = [];
+    this.oversizedColumnHeaders = [];
+    this.hasOversizedColumnHeadersMarked = {};
+    this.clientHeight = 0;
+    this.containerWidth = NaN;
+    this.rowHeaderWidth = NaN;
+    this.rowsVisibleCalculator = null;
+    this.columnsVisibleCalculator = null;
+
+    this.eventManager = new EventManager(this.wot);
+    this.eventManager.addEventListener(window, 'resize', () => {
+      this.clientHeight = this.getWorkspaceHeight();
+    });
+  }
+
+  /**
+   * @returns {number}
+   */
+  getWorkspaceHeight() {
+    const trimmingContainer = this.instance.wtOverlays.topOverlay.trimmingContainer;
+    let elemHeight;
+    let height = 0;
+
+    if (trimmingContainer === window) {
+      height = document.documentElement.clientHeight;
+
+    } else {
+      elemHeight = outerHeight(trimmingContainer);
+      // returns height without DIV scrollbar
+      height = (elemHeight > 0 && trimmingContainer.clientHeight > 0) ? trimmingContainer.clientHeight : Infinity;
+    }
+
+    return height;
+  }
+
+  getWorkspaceWidth() {
+    let width;
+    const totalColumns = this.wot.getSetting('totalColumns');
+    const trimmingContainer = this.instance.wtOverlays.leftOverlay.trimmingContainer;
+    let overflow;
+    const stretchSetting = this.wot.getSetting('stretchH');
+    const docOffsetWidth = document.documentElement.offsetWidth;
+    const preventOverflow = this.wot.getSetting('preventOverflow');
+
+    if (preventOverflow) {
+      return outerWidth(this.instance.wtTable.wtRootElement);
+    }
+
+    if (this.wot.getSetting('freezeOverlays')) {
+      width = Math.min(docOffsetWidth - this.getWorkspaceOffset().left, docOffsetWidth);
+    } else {
+      width = Math.min(this.getContainerFillWidth(), docOffsetWidth - this.getWorkspaceOffset().left, docOffsetWidth);
+    }
+
+    if (trimmingContainer === window && totalColumns > 0 && this.sumColumnWidths(0, totalColumns - 1) > width) {
+      // in case sum of column widths is higher than available stylesheet width, let's assume using the whole window
+      // otherwise continue below, which will allow stretching
+      // this is used in `scroll_window.html`
+      // TODO test me
+      return document.documentElement.clientWidth;
+    }
+
+    if (trimmingContainer !== window) {
+      overflow = getStyle(this.instance.wtOverlays.leftOverlay.trimmingContainer, 'overflow');
+
+      if (overflow === 'scroll' || overflow === 'hidden' || overflow === 'auto') {
+        // this is used in `scroll.html`
+        // TODO test me
+        return Math.max(width, trimmingContainer.clientWidth);
+      }
+    }
+
+    if (stretchSetting === 'none' || !stretchSetting) {
+      // if no stretching is used, return the maximum used workspace width
+      return Math.max(width, outerWidth(this.instance.wtTable.TABLE));
+    }
+
+    // if stretching is used, return the actual container width, so the columns can fit inside it
+    return width;
+  }
+
+  /**
+   * Checks if viewport has vertical scroll
+   *
+   * @returns {Boolean}
+   */
+  hasVerticalScroll() {
+    return this.getWorkspaceActualHeight() > this.getWorkspaceHeight();
+  }
+
+  /**
+   * Checks if viewport has horizontal scroll
+   *
+   * @returns {Boolean}
+   */
+  hasHorizontalScroll() {
+    return this.getWorkspaceActualWidth() > this.getWorkspaceWidth();
+  }
+
+  /**
+   * @param from
+   * @param length
+   * @returns {Number}
+   */
+  sumColumnWidths(from, length) {
+    const wtTable = this.wot.wtTable;
+    let sum = 0;
+    let column = from;
+
+    while (column < length) {
+      sum += wtTable.getColumnWidth(column);
+      column += 1;
+    }
+
+    return sum;
+  }
+
+  /**
+   * @returns {Number}
+   */
+  getContainerFillWidth() {
+    if (this.containerWidth) {
+      return this.containerWidth;
+    }
+
+    const mainContainer = this.instance.wtTable.holder;
+    const dummyElement = document.createElement('div');
+
+    dummyElement.style.width = '100%';
+    dummyElement.style.height = '1px';
+    mainContainer.appendChild(dummyElement);
+
+    const fillWidth = dummyElement.offsetWidth;
+
+    this.containerWidth = fillWidth;
+    mainContainer.removeChild(dummyElement);
+
+    return fillWidth;
+  }
+
+  /**
+   * @returns {Number}
+   */
+  getWorkspaceOffset() {
+    return offset(this.wot.wtTable.TABLE);
+  }
+
+  /**
+   * @returns {Number}
+   */
+  getWorkspaceActualHeight() {
+    return outerHeight(this.wot.wtTable.TABLE);
+  }
+
+  /**
+   * @returns {Number}
+   */
+  getWorkspaceActualWidth() {
+    return outerWidth(this.wot.wtTable.TABLE) ||
+      outerWidth(this.wot.wtTable.TBODY) ||
+      outerWidth(this.wot.wtTable.THEAD); // IE8 reports 0 as <table> offsetWidth;
+  }
+
+  /**
+   * @returns {Number}
+   */
+  getColumnHeaderHeight() {
+    if (isNaN(this.columnHeaderHeight)) {
+      this.columnHeaderHeight = outerHeight(this.wot.wtTable.THEAD);
+    }
+
+    return this.columnHeaderHeight;
+  }
+
+  /**
+   * @returns {Number}
+   */
+  getViewportHeight() {
+    let containerHeight = this.getWorkspaceHeight();
+
+    if (containerHeight === Infinity) {
+      return containerHeight;
+    }
+
+    const columnHeaderHeight = this.getColumnHeaderHeight();
+
+    if (columnHeaderHeight > 0) {
+      containerHeight -= columnHeaderHeight;
+    }
+
+    return containerHeight;
+  }
+
+  /**
+   * @returns {Number}
+   */
+  getRowHeaderWidth() {
+    const rowHeadersHeightSetting = this.instance.getSetting('rowHeaderWidth');
+    const rowHeaders = this.instance.getSetting('rowHeaders');
+
+    if (rowHeadersHeightSetting) {
+      this.rowHeaderWidth = 0;
+
+      for (let i = 0, len = rowHeaders.length; i < len; i++) {
+        this.rowHeaderWidth += rowHeadersHeightSetting[i] || rowHeadersHeightSetting;
+      }
+    }
+
+    if (this.wot.cloneSource) {
+      return this.wot.cloneSource.wtViewport.getRowHeaderWidth();
+    }
+
+    if (isNaN(this.rowHeaderWidth)) {
+
+      if (rowHeaders.length) {
+        let TH = this.instance.wtTable.TABLE.querySelector('TH');
+        this.rowHeaderWidth = 0;
+
+        for (let i = 0, len = rowHeaders.length; i < len; i++) {
+          if (TH) {
+            this.rowHeaderWidth += outerWidth(TH);
+            TH = TH.nextSibling;
+
+          } else {
+            // yes this is a cheat but it worked like that before, just taking assumption from CSS instead of measuring.
+            // TODO: proper fix
+            this.rowHeaderWidth += 50;
+          }
+        }
+      } else {
+        this.rowHeaderWidth = 0;
+      }
+    }
+
+    this.rowHeaderWidth = this.instance.getSetting('onModifyRowHeaderWidth', this.rowHeaderWidth) || this.rowHeaderWidth;
+
+    return this.rowHeaderWidth;
+  }
+
+  /**
+   * @returns {Number}
+   */
+  getViewportWidth() {
+    const containerWidth = this.getWorkspaceWidth();
+
+    if (containerWidth === Infinity) {
+      return containerWidth;
+    }
+
+    const rowHeaderWidth = this.getRowHeaderWidth();
+
+    if (rowHeaderWidth > 0) {
+      return containerWidth - rowHeaderWidth;
+    }
+
+    return containerWidth;
+  }
+
+  /**
+   * Creates:
+   *  - rowsRenderCalculator (before draw, to qualify rows for rendering)
+   *  - rowsVisibleCalculator (after draw, to measure which rows are actually visible)
+   *
+   * @returns {ViewportRowsCalculator}
+   */
+  createRowsCalculator(visible = false) {
+    let height;
+    let scrollbarHeight;
+    let fixedRowsHeight;
+
+    this.rowHeaderWidth = NaN;
+
+    if (this.wot.wtSettings.settings.renderAllRows && !visible) {
+      height = Infinity;
+    } else {
+      height = this.getViewportHeight();
+    }
+
+    let pos = this.wot.wtOverlays.topOverlay.getScrollPosition() - this.wot.wtOverlays.topOverlay.getTableParentOffset();
+
+    if (pos < 0) {
+      pos = 0;
+    }
+
+    const fixedRowsTop = this.wot.getSetting('fixedRowsTop');
+    const fixedRowsBottom = this.wot.getSetting('fixedRowsBottom');
+    const totalRows = this.wot.getSetting('totalRows');
+
+    if (fixedRowsTop) {
+      fixedRowsHeight = this.wot.wtOverlays.topOverlay.sumCellSizes(0, fixedRowsTop);
+      pos += fixedRowsHeight;
+      height -= fixedRowsHeight;
+    }
+
+    if (fixedRowsBottom && this.wot.wtOverlays.bottomOverlay.clone) {
+      fixedRowsHeight = this.wot.wtOverlays.bottomOverlay.sumCellSizes(totalRows - fixedRowsBottom, totalRows);
+
+      height -= fixedRowsHeight;
+    }
+
+    if (this.wot.wtTable.holder.clientHeight === this.wot.wtTable.holder.offsetHeight) {
+      scrollbarHeight = 0;
+    } else {
+      scrollbarHeight = getScrollbarWidth();
+    }
+
+    return new ViewportRowsCalculator(
+      height,
+      pos,
+      this.wot.getSetting('totalRows'),
+      sourceRow => this.wot.wtTable.getRowHeight(sourceRow),
+      visible ? null : this.wot.wtSettings.settings.viewportRowCalculatorOverride,
+      visible,
+      scrollbarHeight
+    );
+  }
+
+  /**
+   * Creates:
+   *  - columnsRenderCalculator (before draw, to qualify columns for rendering)
+   *  - columnsVisibleCalculator (after draw, to measure which columns are actually visible)
+   *
+   * @returns {ViewportRowsCalculator}
+   */
+  createColumnsCalculator(visible = false) {
+    let width = this.getViewportWidth();
+    let pos = this.wot.wtOverlays.leftOverlay.getScrollPosition() - this.wot.wtOverlays.leftOverlay.getTableParentOffset();
+
+    this.columnHeaderHeight = NaN;
+
+    if (pos < 0) {
+      pos = 0;
+    }
+
+    const fixedColumnsLeft = this.wot.getSetting('fixedColumnsLeft');
+
+    if (fixedColumnsLeft) {
+      const fixedColumnsWidth = this.wot.wtOverlays.leftOverlay.sumCellSizes(0, fixedColumnsLeft);
+      pos += fixedColumnsWidth;
+      width -= fixedColumnsWidth;
+    }
+    if (this.wot.wtTable.holder.clientWidth !== this.wot.wtTable.holder.offsetWidth) {
+      width -= getScrollbarWidth();
+    }
+
+    return new ViewportColumnsCalculator(
+      width,
+      pos,
+      this.wot.getSetting('totalColumns'),
+      sourceCol => this.wot.wtTable.getColumnWidth(sourceCol),
+      visible ? null : this.wot.wtSettings.settings.viewportColumnCalculatorOverride,
+      visible,
+      this.wot.getSetting('stretchH'),
+      (stretchedWidth, column) => this.wot.getSetting('onBeforeStretchingColumnWidth', stretchedWidth, column)
+    );
+  }
+
+  /**
+   * Creates rowsRenderCalculator and columnsRenderCalculator (before draw, to determine what rows and
+   * cols should be rendered)
+   *
+   * @param fastDraw {Boolean} If `true`, will try to avoid full redraw and only update the border positions.
+   *                           If `false` or `undefined`, will perform a full redraw
+   * @returns fastDraw {Boolean} The fastDraw value, possibly modified
+   */
+  createRenderCalculators(fastDraw = false) {
+    let runFastDraw = fastDraw;
+
+    if (runFastDraw) {
+      const proposedRowsVisibleCalculator = this.createRowsCalculator(true);
+      const proposedColumnsVisibleCalculator = this.createColumnsCalculator(true);
+
+      if (!(this.areAllProposedVisibleRowsAlreadyRendered(proposedRowsVisibleCalculator) &&
+          this.areAllProposedVisibleColumnsAlreadyRendered(proposedColumnsVisibleCalculator))) {
+        runFastDraw = false;
+      }
+    }
+
+    if (!runFastDraw) {
+      this.rowsRenderCalculator = this.createRowsCalculator();
+      this.columnsRenderCalculator = this.createColumnsCalculator();
+    }
+    // delete temporarily to make sure that renderers always use rowsRenderCalculator, not rowsVisibleCalculator
+    this.rowsVisibleCalculator = null;
+    this.columnsVisibleCalculator = null;
+
+    return runFastDraw;
+  }
+
+  /**
+   * Creates rowsVisibleCalculator and columnsVisibleCalculator (after draw, to determine what are
+   * the actually visible rows and columns)
+   */
+  createVisibleCalculators() {
+    this.rowsVisibleCalculator = this.createRowsCalculator(true);
+    this.columnsVisibleCalculator = this.createColumnsCalculator(true);
+  }
+
+  /**
+   * Returns information whether proposedRowsVisibleCalculator viewport
+   * is contained inside rows rendered in previous draw (cached in rowsRenderCalculator)
+   *
+   * @param {Object} proposedRowsVisibleCalculator
+   * @returns {Boolean} Returns `true` if all proposed visible rows are already rendered (meaning: redraw is not needed).
+   *                    Returns `false` if at least one proposed visible row is not already rendered (meaning: redraw is needed)
+   */
+  areAllProposedVisibleRowsAlreadyRendered(proposedRowsVisibleCalculator) {
+    if (this.rowsVisibleCalculator) {
+      if (proposedRowsVisibleCalculator.startRow < this.rowsRenderCalculator.startRow ||
+          (proposedRowsVisibleCalculator.startRow === this.rowsRenderCalculator.startRow &&
+          proposedRowsVisibleCalculator.startRow > 0)) {
+        return false;
+
+      } else if (proposedRowsVisibleCalculator.endRow > this.rowsRenderCalculator.endRow ||
+          (proposedRowsVisibleCalculator.endRow === this.rowsRenderCalculator.endRow &&
+          proposedRowsVisibleCalculator.endRow < this.wot.getSetting('totalRows') - 1)) {
+        return false;
+
+      }
+      return true;
+
+    }
+
+    return false;
+  }
+
+  /**
+   * Returns information whether proposedColumnsVisibleCalculator viewport
+   * is contained inside column rendered in previous draw (cached in columnsRenderCalculator)
+   *
+   * @param {Object} proposedColumnsVisibleCalculator
+   * @returns {Boolean} Returns `true` if all proposed visible columns are already rendered (meaning: redraw is not needed).
+   *                    Returns `false` if at least one proposed visible column is not already rendered (meaning: redraw is needed)
+   */
+  areAllProposedVisibleColumnsAlreadyRendered(proposedColumnsVisibleCalculator) {
+    if (this.columnsVisibleCalculator) {
+      if (proposedColumnsVisibleCalculator.startColumn < this.columnsRenderCalculator.startColumn ||
+          (proposedColumnsVisibleCalculator.startColumn === this.columnsRenderCalculator.startColumn &&
+          proposedColumnsVisibleCalculator.startColumn > 0)) {
+        return false;
+
+      } else if (proposedColumnsVisibleCalculator.endColumn > this.columnsRenderCalculator.endColumn ||
+          (proposedColumnsVisibleCalculator.endColumn === this.columnsRenderCalculator.endColumn &&
+          proposedColumnsVisibleCalculator.endColumn < this.wot.getSetting('totalColumns') - 1)) {
+        return false;
+
+      }
+      return true;
+
+    }
+
+    return false;
+  }
+
+  /**
+   * Resets values in keys of the hasOversizedColumnHeadersMarked object after updateSettings.
+   */
+  resetHasOversizedColumnHeadersMarked() {
+    objectEach(this.hasOversizedColumnHeadersMarked, (value, key, object) => {
+      object[key] = void 0;
+    });
+  }
+}
+
+export default Viewport;

+ 34 - 0
handsontable/src/3rdparty/walkontable/test/SpecRunner.html

@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Jasmine Spec Runner</title>
+  
+    <link rel="stylesheet" href="../../../../node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
+  
+  
+    <link rel="stylesheet" href="../css/walkontable.css">
+  
+  
+    <script src="../../../../node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
+  
+    <script src="../../../../node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
+  
+    <script src="../../../../node_modules/jasmine-core/lib/jasmine-core/json2.js"></script>
+  
+    <script src="../../../../node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
+  
+  
+    <script src="helpers/jasmine-bridge-reporter.js"></script>
+  
+    <script src="lib/jquery.min.js"></script>
+  
+    <script src="lib/jquery.simulate.js"></script>
+  
+    <script src="../dist/walkontable.js"></script>
+  
+</head>
+<body>
+
+<script type="text/javascript" src="dist/specs.entry.js"></script><script type="text/javascript" src="dist/helpers.entry.js"></script></body>
+</html>

+ 273 - 0
handsontable/src/3rdparty/walkontable/test/helpers/common.js

@@ -0,0 +1,273 @@
+export function sleep(delay = 100) {
+  return Promise.resolve({
+    then(resolve) {
+      if (delay === 0) {
+        setImmediate(resolve);
+      } else {
+        setTimeout(resolve, delay);
+      }
+    }
+  });
+}
+
+let currentSpec;
+
+export function spec() {
+  return currentSpec;
+}
+
+export function createDataArray(rows = 100, cols = 4) {
+  spec().data = [];
+
+  for (let i = 0; i < rows; i++) {
+    const row = [];
+
+    if (cols > 0) {
+      row.push(i);
+
+      for (let j = 0; j < cols - 1; j++) {
+        /* eslint-disable no-mixed-operators */
+        /* eslint-disable no-bitwise */
+        row.push(String.fromCharCode(65 + j % 20).toLowerCase() + (j / 20 | 0 || '')); // | 0 is parseInt - see http://jsperf.com/math-floor-vs-math-round-vs-parseint/18
+      }
+    }
+    spec().data.push(row);
+  }
+}
+
+export function getData(row, col) {
+  return spec().data[row][col];
+}
+
+export function getTotalRows() {
+  return spec().data.length;
+}
+
+export function getTotalColumns() {
+  return spec().data[0] ? spec().data[0].length : 0;
+}
+
+beforeEach(function() {
+  currentSpec = this;
+
+  const matchers = {
+    toBeInArray() {
+      return {
+        compare(actual, expected) {
+          return {
+            pass: Array.isArray(expected) && expected.indexOf(actual) > -1
+          };
+        }
+      };
+    },
+    toBeFunction() {
+      return {
+        compare(actual) {
+          return {
+            pass: typeof actual === 'function'
+          };
+        }
+      };
+    },
+    toBeAroundValue() {
+      return {
+        compare(actual, expected, diff = 1) {
+          const pass = actual >= expected - diff && actual <= expected + diff;
+          let message = `Expected ${actual} to be around ${expected} (between ${expected - diff} and ${expected + diff})`;
+
+          if (!pass) {
+            message = `Expected ${actual} NOT to be around ${expected} (between ${expected - diff} and ${expected + diff})`;
+          }
+
+          return {
+            pass,
+            message
+          };
+        }
+      };
+    }
+  };
+
+  jasmine.addMatchers(matchers);
+});
+
+afterEach(() => {
+  window.scrollTo(0, 0);
+});
+
+export function getTableWidth(elem) {
+  return $(elem).outerWidth() || $(elem).find('tbody').outerWidth() || $(elem).find('thead').outerWidth(); // IE8 reports 0 as <table> offsetWidth
+}
+
+export function range(start, end) {
+  if (!arguments.length) {
+    return [];
+  }
+
+  let from = start;
+  let to = end;
+
+  if (arguments.length === 1) {
+    to = from;
+    from = 0;
+  }
+
+  if (to > from) {
+    from = [to, to = from][0]; // one-liner for swapping two values
+  }
+
+  const result = [];
+
+  while (to < from) {
+    to += 1;
+    result.push(to);
+  }
+
+  return result;
+}
+
+/**
+ * Creates the selection controller necessary for the Walkontable to make selections typical for Handsontable such as
+ * current selection, area selection, selection for autofill and custom borders.
+ *
+ * @param {Object} selections An object with custom selection instances.
+ * @returns {Object} Selection controller.
+ */
+export function createSelectionController({ current, area, fill, custom } = {}) {
+  const currentCtrl = current || new Walkontable.Selection({
+    className: 'current',
+    border: {
+      width: 2,
+      color: '#4b89ff',
+    },
+  });
+  const areaCtrl = area || new Walkontable.Selection({
+    className: 'area',
+    border: {
+      width: 1,
+      color: '#4b89ff',
+    },
+  });
+  const fillCtrl = fill || new Walkontable.Selection({
+    className: 'fill',
+    border: {
+      width: 1,
+      color: '#ff0000',
+    },
+  });
+  const customCtrl = custom || [];
+
+  return {
+    getCell() {
+      return currentCtrl;
+    },
+    createOrGetArea() {
+      return areaCtrl;
+    },
+    getAreas() {
+      return [areaCtrl];
+    },
+    getFill() {
+      return fillCtrl;
+    },
+    [Symbol.iterator]() {
+      return [
+        fillCtrl,
+        currentCtrl,
+        areaCtrl,
+        ...customCtrl,
+      ][Symbol.iterator]();
+    },
+  };
+}
+
+export function getTableTopClone() {
+  return $('.ht_clone_top');
+}
+
+export function getTableLeftClone() {
+  return $('.ht_clone_left');
+}
+
+export function getTableCornerClone() {
+  return $('.ht_clone_top_left_corner');
+}
+
+export function createSpreadsheetData(rows, columns) {
+  const _rows = [];
+  let i;
+  let j;
+
+  for (i = 0; i < rows; i++) {
+    const row = [];
+
+    for (j = 0; j < columns; j++) {
+      row.push(spreadsheetColumnLabel(j) + (i + 1));
+    }
+    _rows.push(row);
+  }
+
+  return _rows;
+}
+
+const COLUMN_LABEL_BASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+const COLUMN_LABEL_BASE_LENGTH = COLUMN_LABEL_BASE.length;
+
+/**
+ * Generates spreadsheet-like column names: A, B, C, ..., Z, AA, AB, etc.
+ *
+ * @param {Number} index Column index.
+ * @returns {String}
+ */
+export function spreadsheetColumnLabel(index) {
+  let dividend = index + 1;
+  let columnLabel = '';
+  let modulo;
+
+  while (dividend > 0) {
+    modulo = (dividend - 1) % COLUMN_LABEL_BASE_LENGTH;
+    columnLabel = String.fromCharCode(65 + modulo) + columnLabel;
+    dividend = parseInt((dividend - modulo) / COLUMN_LABEL_BASE_LENGTH, 10);
+  }
+
+  return columnLabel;
+}
+
+export function walkontableCalculateScrollbarWidth() {
+  const inner = document.createElement('div');
+  inner.style.height = '200px';
+  inner.style.width = '100%';
+
+  const outer = document.createElement('div');
+  outer.style.boxSizing = 'content-box';
+  outer.style.height = '150px';
+  outer.style.left = '0px';
+  outer.style.overflow = 'hidden';
+  outer.style.position = 'absolute';
+  outer.style.top = '0px';
+  outer.style.width = '200px';
+  outer.style.visibility = 'hidden';
+  outer.appendChild(inner);
+
+  (document.body || document.documentElement).appendChild(outer);
+  const w1 = inner.offsetWidth;
+  outer.style.overflow = 'scroll';
+  let w2 = inner.offsetWidth;
+  if (w1 === w2) {
+    w2 = outer.clientWidth;
+  }
+
+  (document.body || document.documentElement).removeChild(outer);
+
+  return (w1 - w2);
+}
+
+let cachedScrollbarWidth;
+
+export function getScrollbarWidth() {
+  if (cachedScrollbarWidth === void 0) {
+    cachedScrollbarWidth = walkontableCalculateScrollbarWidth();
+  }
+
+  return cachedScrollbarWidth;
+}

+ 8 - 0
handsontable/src/3rdparty/walkontable/test/helpers/index.js

@@ -0,0 +1,8 @@
+/* eslint-disable import/no-unresolved */
+import window from 'window';
+import * as common from './common';
+
+// Export all helpers to the window.
+Object.keys(common).forEach((key) => {
+  window[key] = common[key];
+});

+ 47 - 0
handsontable/src/3rdparty/walkontable/test/helpers/jasmine-bridge-reporter.js

@@ -0,0 +1,47 @@
+(function() {
+  if (typeof jasmineStarted === 'undefined') {
+    return;
+  }
+
+  function JasmineBridgeReporter() {
+    this.started = false;
+    this.finished = false;
+    this.suites_ = [];
+    this.results_ = {};
+    this.buffer = '';
+  }
+
+  JasmineBridgeReporter.prototype.jasmineStarted = function(metadata) {
+    this.started = true;
+    jasmineStarted(metadata);
+  };
+
+  JasmineBridgeReporter.prototype.specStarted = function(specMetadata) {
+    specMetadata.startTime = Date.now();
+    jasmineSpecStarted(specMetadata);
+  };
+
+  JasmineBridgeReporter.prototype.suiteStarted = function(suiteMetadata) {
+    suiteMetadata.startTime = Date.now();
+    jasmineSuiteStarted(suiteMetadata);
+  };
+
+  JasmineBridgeReporter.prototype.jasmineDone = function() {
+    this.finished = true;
+    jasmineDone();
+  };
+
+  JasmineBridgeReporter.prototype.suiteDone = function(suiteMetadata) {
+    suiteMetadata.duration = Date.now() - suiteMetadata.startTime;
+    jasmineSuiteDone(suiteMetadata);
+  };
+
+  JasmineBridgeReporter.prototype.specDone = function(specMetadata) {
+    specMetadata.duration = Date.now() - specMetadata.startTime;
+    this.results_[specMetadata.id] = specMetadata;
+
+    jasmineSpecDone(specMetadata);
+  };
+
+  jasmine.getEnv().addReporter(new JasmineBridgeReporter());
+}());

Fichier diff supprimé car celui-ci est trop grand
+ 2 - 0
handsontable/src/3rdparty/walkontable/test/lib/jquery.min.js


+ 337 - 0
handsontable/src/3rdparty/walkontable/test/lib/jquery.simulate.js

@@ -0,0 +1,337 @@
+/*!
+ * jQuery Simulate v@VERSION - simulate browser mouse and keyboard events
+ * https://github.com/jquery/jquery-simulate
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * Date: @DATE
+ */
+
+;(function( $, undefined ) {
+
+  var rkeyEvent = /^key/,
+    rmouseEvent = /^(?:mouse|contextmenu)|click/;
+
+  $.fn.simulate = function( type, options ) {
+    return this.each(function() {
+      new $.simulate( this, type, options );
+    });
+  };
+
+  $.simulate = function( elem, type, options ) {
+    var method = $.camelCase( "simulate-" + type );
+
+    this.target = elem;
+    this.options = options;
+
+    if ( this[ method ] ) {
+      this[ method ]();
+    } else {
+      this.simulateEvent( elem, type, options );
+    }
+  };
+
+  $.extend( $.simulate, {
+
+    keyCode: {
+      BACKSPACE: 8,
+      COMMA: 188,
+      DELETE: 46,
+      DOWN: 40,
+      END: 35,
+      ENTER: 13,
+      ESCAPE: 27,
+      HOME: 36,
+      LEFT: 37,
+      NUMPAD_ADD: 107,
+      NUMPAD_DECIMAL: 110,
+      NUMPAD_DIVIDE: 111,
+      NUMPAD_ENTER: 108,
+      NUMPAD_MULTIPLY: 106,
+      NUMPAD_SUBTRACT: 109,
+      PAGE_DOWN: 34,
+      PAGE_UP: 33,
+      PERIOD: 190,
+      RIGHT: 39,
+      SPACE: 32,
+      TAB: 9,
+      UP: 38
+    },
+
+    buttonCode: {
+      LEFT: 0,
+      MIDDLE: 1,
+      RIGHT: 2
+    }
+  });
+
+  $.extend( $.simulate.prototype, {
+
+    simulateEvent: function( elem, type, options ) {
+      var event = this.createEvent( type, options );
+      this.dispatchEvent( elem, type, event, options );
+    },
+
+    createEvent: function( type, options ) {
+      if ( rkeyEvent.test( type ) ) {
+        return this.keyEvent( type, options );
+      }
+
+      if ( rmouseEvent.test( type ) ) {
+        return this.mouseEvent( type, options );
+      }
+    },
+
+    mouseEvent: function( type, options ) {
+      var event, eventDoc, doc, body;
+      options = $.extend({
+        bubbles: true,
+        cancelable: (type !== "mousemove"),
+        view: window,
+        detail: 0,
+        screenX: 0,
+        screenY: 0,
+        clientX: 1,
+        clientY: 1,
+        ctrlKey: false,
+        altKey: false,
+        shiftKey: false,
+        metaKey: false,
+        button: 0,
+        relatedTarget: undefined
+      }, options );
+
+      if ( document.createEvent ) {
+        event = document.createEvent( "MouseEvents" );
+        event.initMouseEvent( type, options.bubbles, options.cancelable,
+          options.view, options.detail,
+          options.screenX, options.screenY, options.clientX, options.clientY,
+          options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
+          options.button, options.relatedTarget || document.body.parentNode );
+
+        // IE 9+ creates events with pageX and pageY set to 0.
+        // Trying to modify the properties throws an error,
+        // so we define getters to return the correct values.
+        if ( event.pageX === 0 && event.pageY === 0 && Object.defineProperty ) {
+          eventDoc = event.relatedTarget.ownerDocument || document;
+          doc = eventDoc.documentElement;
+          body = eventDoc.body;
+
+          Object.defineProperty( event, "pageX", {
+            get: function() {
+              return options.clientX +
+                ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
+                ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+            }
+          });
+          Object.defineProperty( event, "pageY", {
+            get: function() {
+              return options.clientY +
+                ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
+                ( doc && doc.clientTop || body && body.clientTop || 0 );
+            }
+          });
+        }
+      } else if ( document.createEventObject ) {
+
+        try {
+          event = document.createEventObject(options);
+        } catch (e) {
+          event = document.createEventObject();
+          $.extend( event, options );
+        }
+
+
+        // standards event.button uses constants defined here: http://msdn.microsoft.com/en-us/library/ie/ff974877(v=vs.85).aspx
+        // old IE event.button uses constants defined here: http://msdn.microsoft.com/en-us/library/ie/ms533544(v=vs.85).aspx
+        // so we actually need to map the standard back to oldIE
+        event.button = {
+          0: 1,
+          1: 4,
+          2: 2
+        }[ event.button ] || ( event.button === -1 ? 0 : event.button );
+      }
+
+      return event;
+    },
+
+    keyEvent: function( type, options ) {
+      var event;
+      options = $.extend({
+        bubbles: true,
+        cancelable: true,
+        view: window,
+        ctrlKey: false,
+        altKey: false,
+        shiftKey: false,
+        metaKey: false,
+        keyCode: 0,
+        charCode: undefined
+      }, options );
+
+      if ( document.createEvent ) {
+        try {
+          event = document.createEvent( "KeyEvents" );
+          event.initKeyEvent( type, options.bubbles, options.cancelable, options.view,
+            options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
+            options.keyCode, options.charCode );
+          // initKeyEvent throws an exception in WebKit
+          // see: http://stackoverflow.com/questions/6406784/initkeyevent-keypress-only-works-in-firefox-need-a-cross-browser-solution
+          // and also https://bugs.webkit.org/show_bug.cgi?id=13368
+          // fall back to a generic event until we decide to implement initKeyboardEvent
+        } catch( err ) {
+          event = document.createEvent( "Events" );
+          event.initEvent( type, options.bubbles, options.cancelable );
+          $.extend( event, {
+            view: options.view,
+            ctrlKey: options.ctrlKey,
+            altKey: options.altKey,
+            shiftKey: options.shiftKey,
+            metaKey: options.metaKey,
+            keyCode: options.keyCode,
+            charCode: options.charCode
+          });
+        }
+      } else if ( document.createEventObject ) {
+        event = document.createEventObject();
+        $.extend( event, options );
+      }
+
+      if ( !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ) || (({}).toString.call( window.opera ) === "[object Opera]") ) {
+        event.keyCode = (options.charCode > 0) ? options.charCode : options.keyCode;
+        event.charCode = undefined;
+      }
+
+      return event;
+    },
+
+    dispatchEvent: function( elem, type, event ) {
+      if ( elem[ type ] ) {
+        elem[ type ]();
+      } else if ( elem.dispatchEvent ) {
+        elem.dispatchEvent( event );
+      } else if ( elem.fireEvent ) {
+        elem.fireEvent( "on" + type, event );
+      }
+    },
+
+    simulateFocus: function() {
+      var focusinEvent,
+        triggered = false,
+        element = $( this.target );
+
+      function trigger() {
+        triggered = true;
+      }
+
+      element.bind( "focus", trigger );
+      element[ 0 ].focus();
+
+      if ( !triggered ) {
+        focusinEvent = $.Event( "focusin" );
+        focusinEvent.preventDefault();
+        element.trigger( focusinEvent );
+        element.triggerHandler( "focus" );
+      }
+      element.unbind( "focus", trigger );
+    },
+
+    simulateBlur: function() {
+      var focusoutEvent,
+        triggered = false,
+        element = $( this.target );
+
+      function trigger() {
+        triggered = true;
+      }
+
+      element.bind( "blur", trigger );
+      element[ 0 ].blur();
+
+      // blur events are async in IE
+      setTimeout(function() {
+        // IE won't let the blur occur if the window is inactive
+        if ( element[ 0 ].ownerDocument.activeElement === element[ 0 ] ) {
+          element[ 0 ].ownerDocument.body.focus();
+        }
+
+        // Firefox won't trigger events if the window is inactive
+        // IE doesn't trigger events if we had to manually focus the body
+        if ( !triggered ) {
+          focusoutEvent = $.Event( "focusout" );
+          focusoutEvent.preventDefault();
+          element.trigger( focusoutEvent );
+          element.triggerHandler( "blur" );
+        }
+        element.unbind( "blur", trigger );
+      }, 1 );
+    }
+  });
+
+
+
+  /** complex events **/
+
+  function findCenter( elem ) {
+    var offset,
+      document = $( elem.ownerDocument );
+    elem = $( elem );
+    offset = elem.offset();
+
+    return {
+      x: offset.left + elem.outerWidth() / 2 - document.scrollLeft(),
+      y: offset.top + elem.outerHeight() / 2 - document.scrollTop()
+    };
+  }
+
+  function findCorner( elem ) {
+    var offset,
+      document = $( elem.ownerDocument );
+    elem = $( elem );
+    offset = elem.offset();
+
+    return {
+      x: offset.left - document.scrollLeft(),
+      y: offset.top - document.scrollTop()
+    };
+  }
+
+  $.extend( $.simulate.prototype, {
+    simulateDrag: function() {
+      var i = 0,
+        target = this.target,
+        options = this.options,
+        center = options.handle === "corner" ? findCorner( target ) : findCenter( target ),
+        x = Math.floor( center.x ),
+        y = Math.floor( center.y ),
+        coord = { clientX: x, clientY: y },
+        dx = options.dx || ( options.x !== undefined ? options.x - x : 0 ),
+        dy = options.dy || ( options.y !== undefined ? options.y - y : 0 ),
+        moves = options.moves || 3;
+
+      this.simulateEvent( target, "mousedown", coord );
+
+      for ( ; i < moves ; i++ ) {
+        x += dx / moves;
+        y += dy / moves;
+
+        coord = {
+          clientX: Math.round( x ),
+          clientY: Math.round( y )
+        };
+
+        this.simulateEvent( target.ownerDocument, "mousemove", coord );
+      }
+
+      if ( $.contains( document, target ) ) {
+        this.simulateEvent( target, "mouseup", coord );
+        this.simulateEvent( target, "click", coord );
+      } else {
+        this.simulateEvent( document, "mouseup", coord );
+      }
+    }
+  });
+
+})( jQuery );

+ 411 - 0
handsontable/src/3rdparty/walkontable/test/spec/border.spec.js

@@ -0,0 +1,411 @@
+describe('WalkontableBorder', () => {
+  let $table;
+  let $container;
+  let $wrapper;
+  const debug = false;
+
+  beforeEach(() => {
+    $container = $('<div></div>');
+    $wrapper = $('<div></div>');
+    $container.width(100).height(200);
+    $table = $('<table></table>');
+    $container.append($wrapper);
+    $wrapper.append($table);
+    $container.appendTo('body');
+    createDataArray();
+  });
+
+  afterEach(() => {
+    if (!debug) {
+      $('.wtHolder').remove();
+    }
+    $container.remove();
+  });
+
+  it('should add/remove border to selection when cell is clicked', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: 5,
+      totalColumns: 5,
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          border: {
+            width: 1,
+            color: 'red'
+          }
+        })
+      }),
+      onCellMouseDown(event, coords) {
+        wt.selections.getCell().clear();
+        wt.selections.getCell().add(coords);
+        wt.draw();
+      }
+    });
+    wt.draw();
+
+    const $td1 = $table.find('tbody tr:eq(1) td:eq(0)');
+
+    const $td2 = $table.find('tbody tr:eq(2) td:eq(1)');
+    const $top = $(wt.selections.getCell().getBorder(wt).top);
+    const $right = $(wt.selections.getCell().getBorder(wt).right);
+    const $bottom = $(wt.selections.getCell().getBorder(wt).bottom);
+    const $left = $(wt.selections.getCell().getBorder(wt).left);
+
+    $td1.simulate('mousedown');
+
+    expect($top.css('height')).toBe('1px');
+    expect($top.position().top).toBe(23);
+    expect($top.position().left).toBe(0);
+    expect($right.css('width')).toBe('1px');
+    expect($right.position().top).toBe(23);
+    expect($right.position().left).toBe(49);
+    expect($bottom.css('height')).toBe('1px');
+    expect($bottom.position().top).toBe(46);
+    expect($bottom.position().left).toBe(0);
+    expect($left.css('width')).toBe('1px');
+    expect($left.position().top).toBe(23);
+    expect($left.position().left).toBe(0);
+
+    $td2.simulate('mousedown');
+
+    expect($top.css('height')).toBe('1px');
+    expect($top.position().top).toBe(46);
+    expect($top.position().left).toBe(49);
+    expect($right.css('width')).toBe('1px');
+    expect($right.position().top).toBe(46);
+    expect($right.position().left).toBe(99);
+    expect($bottom.css('height')).toBe('1px');
+    expect($bottom.position().top).toBe(69);
+    expect($bottom.position().left).toBe(49);
+    expect($left.css('width')).toBe('1px');
+    expect($left.position().top).toBe(46);
+    expect($left.position().left).toBe(49);
+  });
+
+  it('should add/remove border to selection when cell is clicked and the table has only one column', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: 5,
+      totalColumns: 1,
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          border: {
+            width: 1,
+            color: 'red'
+          }
+        })
+      }),
+      onCellMouseDown(event, coords) {
+        wt.selections.getCell().clear();
+        wt.selections.getCell().add(coords);
+        wt.draw();
+      }
+    });
+    wt.draw();
+
+    const $td1 = $table.find('tbody tr:eq(1) td:eq(0)');
+    const $top = $(wt.selections.getCell().getBorder(wt).top);
+    const $right = $(wt.selections.getCell().getBorder(wt).right);
+    const $bottom = $(wt.selections.getCell().getBorder(wt).bottom);
+    const $left = $(wt.selections.getCell().getBorder(wt).left);
+
+    $td1.simulate('mousedown');
+
+    expect($top.css('height')).toBe('1px');
+    expect($top.position().top).toBe(23);
+    expect($top.position().left).toBe(0);
+    expect($right.css('width')).toBe('1px');
+    expect($right.position().top).toBe(23);
+    expect($right.position().left).toBe(49);
+    expect($bottom.css('height')).toBe('1px');
+    expect($bottom.position().top).toBe(46);
+    expect($bottom.position().left).toBe(0);
+    expect($left.css('width')).toBe('1px');
+    expect($left.position().top).toBe(23);
+    expect($left.position().left).toBe(0);
+  });
+
+  it('should properly add a selection border on an entirely selected column', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: 5,
+      totalColumns: 2,
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          border: {
+            width: 1,
+            color: 'red'
+          }
+        })
+      }),
+      onCellMouseDown(event, coords) {
+        wt.selections.getCell().clear();
+        wt.selections.getCell().add(coords);
+        wt.draw();
+      }
+    });
+    wt.draw();
+
+    wt.selections.getCell().add(new Walkontable.CellCoords(0, 0));
+    wt.selections.getCell().add(new Walkontable.CellCoords(4, 0));
+    wt.draw(true);
+
+    const $top = $(wt.selections.getCell().getBorder(wt).top);
+    const $right = $(wt.selections.getCell().getBorder(wt).right);
+    const $bottom = $(wt.selections.getCell().getBorder(wt).bottom);
+    const $left = $(wt.selections.getCell().getBorder(wt).left);
+
+    expect($top.css('height')).toBe('1px');
+    expect($top.position().top).toBe(0);
+    expect($top.position().left).toBe(0);
+    expect($right.css('width')).toBe('1px');
+    expect($right.position().top).toBe(0);
+    expect($right.position().left).toBe(49);
+    expect($bottom.css('height')).toBe('1px');
+    expect($bottom.position().top).toBe(115);
+    expect($bottom.position().left).toBe(0);
+    expect($left.css('width')).toBe('1px');
+    expect($left.position().top).toBe(0);
+    expect($left.position().left).toBe(0);
+  });
+
+  it('should add/remove corner to selection when cell is clicked', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: 5,
+      totalColumns: 5,
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          border: {
+            width: 2,
+            color: 'green',
+            cornerVisible() {
+              return true;
+            }
+          }
+        }),
+        area: new Walkontable.Selection({}),
+      }),
+      onCellMouseDown(event, coords) {
+        wt.selections.getCell().clear();
+        wt.selections.getCell().add(coords);
+        wt.draw();
+      }
+    });
+    wt.draw();
+
+    const $td1 = $table.find('tbody tr:eq(1) td:eq(0)');
+    const $td2 = $table.find('tbody tr:eq(2) td:eq(1)');
+    const $corner = $(wt.selections.getCell().getBorder(wt).corner);
+
+    $td1.simulate('mousedown');
+
+    expect($corner.css('width')).toBe('6px');
+    expect($corner.css('height')).toBe('6px');
+    expect($corner.position().top).toBe(42);
+    expect($corner.position().left).toBe(45);
+
+    $td2.simulate('mousedown');
+
+    expect($corner.css('width')).toBe('6px');
+    expect($corner.css('height')).toBe('6px');
+    expect($corner.position().top).toBe(65);
+    expect($corner.position().left).toBe(95);
+  });
+
+  it('should draw only one corner if selection is added between overlays', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: 5,
+      totalColumns: 5,
+      fixedColumnsLeft: 2,
+      fixedRowsTop: 2,
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          className: 'current',
+        }),
+        area: new Walkontable.Selection({
+          className: 'area',
+          border: {
+            cornerVisible() {
+              return true;
+            }
+          }
+        }),
+      }),
+    });
+
+    wt.selections.createOrGetArea().add(new Walkontable.CellCoords(0, 0));
+    wt.selections.createOrGetArea().add(new Walkontable.CellCoords(2, 2));
+
+    wt.draw();
+
+    const corners = $container.find('.wtBorder.corner:visible');
+
+    expect(corners.length).toBe(1);
+  });
+
+  it('should move the fill handle / corner border to the left, if in the position it would overlap the container (e.g.: far-right)', () => {
+    $container.css({
+      overflow: 'hidden',
+      width: '200px',
+    });
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: 5,
+      totalColumns: 4,
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          border: {
+            width: 2,
+            color: 'green',
+            cornerVisible() {
+              return true;
+            }
+          }
+        }),
+        area: new Walkontable.Selection({}),
+      }),
+      onCellMouseDown(event, coords) {
+        wt.selections.getCell().clear();
+        wt.selections.getCell().add(coords);
+        wt.draw();
+      }
+    });
+    wt.draw();
+
+    const $td1 = $table.find('tbody tr:eq(1) td:eq(0)');
+    const $td2 = $table.find('tbody tr:eq(3) td:eq(3)');
+    const $td3 = $table.find('tbody tr:eq(2) td:eq(1)');
+    const $corner = $(wt.selections.getCell().getBorder(wt).corner);
+
+    $td1.simulate('mousedown');
+
+    expect($corner.css('width')).toBe('6px');
+    expect($corner.css('height')).toBe('6px');
+    expect($corner.position().top).toBe(42);
+    expect($corner.position().left).toBe(45);
+    expect($container[0].clientWidth === $container[0].scrollWidth).toBe(true);
+
+    $td2.simulate('mousedown');
+
+    expect($corner.css('width')).toBe('6px');
+    expect($corner.css('height')).toBe('6px');
+    expect($corner.position().top).toBe(88);
+    expect($corner.position().left).toBe(193);
+    expect($container[0].clientWidth === $container[0].scrollWidth).toBe(true);
+
+    $td3.simulate('mousedown');
+
+    expect($corner.css('width')).toBe('6px');
+    expect($corner.css('height')).toBe('6px');
+    expect($corner.position().top).toBe(65);
+    expect($corner.position().left).toBe(95);
+    expect($container[0].clientWidth === $container[0].scrollWidth).toBe(true);
+  });
+
+  it('should move the fill handle / corner border to the top, if in the position it would overlap the container (e.g.: far-bottom)', () => {
+    $container.css({
+      overflow: 'hidden',
+      height: 'auto',
+      marginTop: '2000px',
+    });
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: 5,
+      totalColumns: 1,
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          border: {
+            width: 2,
+            color: 'green',
+            cornerVisible() {
+              return true;
+            }
+          }
+        }),
+        area: new Walkontable.Selection({}),
+      }),
+      onCellMouseDown(event, coords) {
+        wt.selections.getCell().clear();
+        wt.selections.getCell().add(coords);
+        wt.draw();
+      }
+    });
+
+    wt.draw();
+
+    const $td = $table.find('tbody tr:last-of-type td:last-of-type');
+    const $corner = $(wt.selections.getCell().getBorder(wt).corner);
+
+    $td.simulate('mousedown');
+
+    wt.draw();
+
+    expect($corner.css('width')).toBe('6px');
+    expect($corner.css('height')).toBe('6px');
+    expect($table.css('height')).toBe('116px');
+    expect($corner.position().top).toBe(109); // table.height - corner.height - corner.borderTop
+    expect($corner.position().left).toBe(45);
+    expect($container[0].clientHeight === $container[0].scrollHeight).toBe(true);
+  });
+
+  it('should move the corner border to the top-left, if is not enough area on the bottom-right corner of container', () => {
+    $container.css({
+      overflow: 'hidden',
+      height: 'auto',
+      width: '50px',
+      marginTop: '2000px',
+      marginLeft: '2000px',
+    });
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: 1,
+      totalColumns: 1,
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          border: {
+            width: 2,
+            color: 'green',
+            cornerVisible() {
+              return true;
+            }
+          }
+        }),
+        area: new Walkontable.Selection({}),
+      }),
+      onCellMouseDown(event, coords) {
+        wt.selections.getCell().clear();
+        wt.selections.getCell().add(coords);
+        wt.draw();
+      }
+    });
+
+    wt.draw();
+
+    const $td = $table.find('tbody tr:last-of-type td:last-of-type');
+    const $corner = $(wt.selections.getCell().getBorder(wt).corner);
+
+    $td.simulate('mousedown');
+
+    wt.draw();
+
+    expect($corner.css('width')).toBe('6px');
+    expect($corner.css('height')).toBe('6px');
+    expect($table.css('height')).toBe('24px');
+    expect($corner.position().top).toBe(17); // table.height - corner.height - corner.borderTop
+    expect($corner.position().left).toBe(43);
+    expect($container[0].clientHeight === $container[0].scrollHeight).toBe(true);
+    expect($container[0].clientWidth === $container[0].scrollWidth).toBe(true);
+  });
+});

+ 226 - 0
handsontable/src/3rdparty/walkontable/test/spec/calculator/viewportColumns.spec.js

@@ -0,0 +1,226 @@
+describe('Walkontable.ViewportColumnsCalculator', () => {
+  function allColumns20() {
+    return 20;
+  }
+
+  it('should render first 5 columns in unscrolled container', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(100, 0, 1000, allColumns20);
+    const visibleCalc = new Walkontable.ViewportColumnsCalculator(100, 0, 1000, allColumns20, null, true);
+
+    expect(calc.startColumn).toBe(0);
+    expect(calc.startPosition).toBe(0);
+    expect(calc.endColumn).toBe(4);
+
+    expect(visibleCalc.startColumn).toBe(0);
+    expect(visibleCalc.endColumn).toBe(4);
+  });
+
+  it('should render 6 columns, starting from 3 in container scrolled to half of fourth column', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(100, 70, 1000, allColumns20);
+    const visibleCalc = new Walkontable.ViewportColumnsCalculator(100, 70, 1000, allColumns20, null, true);
+
+    expect(calc.startColumn).toBe(3);
+    expect(calc.startPosition).toBe(60);
+    expect(calc.endColumn).toBe(8);
+
+    expect(visibleCalc.startColumn).toBe(4);
+    expect(visibleCalc.endColumn).toBe(7);
+  });
+
+  it('should render 10 columns, starting from 1 in container scrolled to half of fourth column (with render overrides)', () => {
+    const overrideFn = function(calc) {
+      calc.startColumn -= 2;
+      calc.endColumn += 2;
+    };
+    const calc = new Walkontable.ViewportColumnsCalculator(100, 70, 1000, allColumns20, overrideFn);
+    const visibleCalc = new Walkontable.ViewportColumnsCalculator(100, 70, 1000, allColumns20, null, true);
+
+    expect(calc.startColumn).toBe(1);
+    expect(calc.startPosition).toBe(20);
+    expect(calc.endColumn).toBe(10);
+
+    expect(visibleCalc.startColumn).toBe(4);
+    expect(visibleCalc.endColumn).toBe(7);
+  });
+
+  it('should return number of rendered columns', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(100, 50, 1000, allColumns20);
+    const visibleCalc = new Walkontable.ViewportColumnsCalculator(100, 50, 1000, allColumns20, null, true);
+
+    expect(calc.count).toBe(6);
+
+    expect(visibleCalc.count).toBe(4);
+  });
+
+  it('should render all columns if their size is smaller than viewport', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(200, 0, 8, allColumns20);
+    const visibleCalc = new Walkontable.ViewportColumnsCalculator(200, 0, 8, allColumns20, null, true);
+
+    expect(calc.startColumn).toBe(0);
+    expect(calc.endColumn).toBe(7);
+    expect(calc.count).toBe(8);
+
+    expect(visibleCalc.startColumn).toBe(0);
+    expect(visibleCalc.endColumn).toBe(7);
+    expect(visibleCalc.count).toBe(8);
+  });
+
+  it('should render all columns if their size is exactly the viewport', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(200, 0, 10, allColumns20);
+    const visibleCalc = new Walkontable.ViewportColumnsCalculator(200, 0, 10, allColumns20, null, true);
+
+    expect(calc.startColumn).toBe(0);
+    expect(calc.endColumn).toBe(9);
+    expect(calc.count).toBe(10);
+
+    expect(visibleCalc.startColumn).toBe(0);
+    expect(visibleCalc.endColumn).toBe(9);
+    expect(visibleCalc.count).toBe(10);
+  });
+
+  it('should render all columns if their size is slightly larger than viewport', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(199, 0, 10, allColumns20);
+    const visibleCalc = new Walkontable.ViewportColumnsCalculator(199, 0, 10, allColumns20, null, true);
+
+    expect(calc.startColumn).toBe(0);
+    expect(calc.endColumn).toBe(9);
+    expect(calc.count).toBe(10);
+
+    expect(visibleCalc.startColumn).toBe(0);
+    expect(visibleCalc.endColumn).toBe(8);
+    expect(visibleCalc.count).toBe(9);
+  });
+
+  it('should set null values if total columns is 0', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(200, 0, 0, allColumns20);
+    const visibleCalc = new Walkontable.ViewportColumnsCalculator(200, 0, 0, allColumns20, null, true);
+
+    expect(calc.startColumn).toBe(null);
+    expect(calc.startPosition).toBe(null);
+    expect(calc.endColumn).toBe(null);
+    expect(calc.count).toBe(0);
+
+    expect(visibleCalc.startColumn).toBe(null);
+    expect(visibleCalc.endColumn).toBe(null);
+  });
+
+  it('should set null values if total columns is 0 (with overrideFn provided)', () => {
+    const overrideFn = function(myCalc) {
+      myCalc.startColumn = 0;
+      myCalc.endColumn = 0;
+    };
+    const calc = new Walkontable.ViewportColumnsCalculator(200, 0, 0, allColumns20, overrideFn);
+    const visibleCalc = new Walkontable.ViewportColumnsCalculator(200, 0, 0, allColumns20, null, true);
+
+    expect(calc.startColumn).toBe(null);
+    expect(calc.startPosition).toBe(null);
+    expect(calc.endColumn).toBe(null);
+    expect(calc.count).toBe(0);
+
+    expect(visibleCalc.startColumn).toBe(null);
+    expect(visibleCalc.endColumn).toBe(null);
+  });
+
+  it('should scroll backwards if total columns is reached', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(190, 350, 20, allColumns20);
+    const visibleCalc = new Walkontable.ViewportColumnsCalculator(190, 350, 20, allColumns20, null, true);
+
+    expect(calc.startColumn).toBe(10);
+    expect(calc.startPosition).toBe(200);
+    expect(calc.endColumn).toBe(19);
+    expect(calc.count).toBe(10);
+
+    expect(visibleCalc.startColumn).toBe(11);
+    expect(visibleCalc.endColumn).toBe(19);
+  });
+
+  it('should update stretchAllRatio after refreshStretching call (stretch: all)', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(250, 0, 20, allColumns20, null, true, 'all');
+
+    expect(calc.stretchAllRatio).toBe(0);
+    expect(calc.stretchLastWidth).toBe(0);
+
+    calc.refreshStretching(414);
+
+    expect(calc.stretchAllRatio).toBe(1.035);
+    expect(calc.stretchLastWidth).toBe(0);
+  });
+
+  it('should update stretchAllRatio after refreshStretching call (stretch: last)', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(250, 0, 5, allColumns20, null, true, 'last');
+
+    expect(calc.stretchAllRatio).toBe(0);
+    expect(calc.stretchLastWidth).toBe(0);
+
+    calc.refreshStretching(414);
+
+    expect(calc.stretchAllRatio).toBe(0);
+    expect(calc.stretchLastWidth).toBe(334);
+  });
+
+  it('should return valid stretched column width (stretch: all)', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(250, 0, 5, allColumns20, null, true, 'all');
+
+    expect(calc.getStretchedColumnWidth(0, 50)).toBe(null);
+    expect(calc.needVerifyLastColumnWidth).toBe(true);
+
+    calc.refreshStretching(417);
+
+    expect(calc.getStretchedColumnWidth(0, allColumns20())).toBe(83);
+    expect(calc.getStretchedColumnWidth(1, allColumns20())).toBe(83);
+    expect(calc.getStretchedColumnWidth(2, allColumns20())).toBe(83);
+    expect(calc.getStretchedColumnWidth(3, allColumns20())).toBe(83);
+    expect(calc.needVerifyLastColumnWidth).toBe(true);
+    expect(calc.getStretchedColumnWidth(4, allColumns20())).toBe(85);
+    expect(calc.needVerifyLastColumnWidth).toBe(false);
+  });
+
+  it('should return valid stretched column width (stretch: last)', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(250, 0, 5, allColumns20, null, true, 'last');
+
+    expect(calc.getStretchedColumnWidth(0, 50)).toBe(null);
+
+    calc.refreshStretching(417);
+
+    expect(calc.getStretchedColumnWidth(0, allColumns20())).toBe(null);
+    expect(calc.getStretchedColumnWidth(1, allColumns20())).toBe(null);
+    expect(calc.getStretchedColumnWidth(2, allColumns20())).toBe(null);
+    expect(calc.getStretchedColumnWidth(3, allColumns20())).toBe(null);
+    expect(calc.getStretchedColumnWidth(4, allColumns20())).toBe(337);
+  });
+
+  it('call refreshStretching should clear stretchAllColumnsWidth and needVerifyLastColumnWidth property', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(250, 0, 5, allColumns20, null, true, 'all');
+
+    expect(calc.stretchAllColumnsWidth.length).toBe(0);
+    expect(calc.needVerifyLastColumnWidth).toBe(true);
+
+    calc.refreshStretching(417);
+    calc.getStretchedColumnWidth(0, allColumns20());
+    calc.getStretchedColumnWidth(1, allColumns20());
+    calc.getStretchedColumnWidth(2, allColumns20());
+    calc.getStretchedColumnWidth(3, allColumns20());
+    calc.getStretchedColumnWidth(4, allColumns20());
+
+    expect(calc.stretchAllColumnsWidth.length).toBe(5);
+    expect(calc.needVerifyLastColumnWidth).toBe(false);
+
+    calc.refreshStretching(201);
+
+    expect(calc.stretchAllColumnsWidth.length).toBe(0);
+    expect(calc.needVerifyLastColumnWidth).toBe(true);
+  });
+
+  it('should calculate the number of columns based on a default width, ' +
+    'when the width returned from the function is not a number', () => {
+    const calc = new Walkontable.ViewportColumnsCalculator(200, 0, 1000, () => (void 0 + 1));
+    const visibleCalc = new Walkontable.ViewportColumnsCalculator(200, 0, 1000, () => (void 0 + 1), null, true);
+
+    expect(calc.startColumn).toBe(0);
+    expect(calc.startPosition).toBe(0);
+    expect(calc.endColumn).toBe(3);
+
+    expect(visibleCalc.startColumn).toBe(0);
+    expect(visibleCalc.endColumn).toBe(3);
+  });
+});

+ 149 - 0
handsontable/src/3rdparty/walkontable/test/spec/calculator/viewportRows.spec.js

@@ -0,0 +1,149 @@
+describe('Walkontable.ViewportRowsCalculator', () => {
+  function allRows20() {
+    return 20;
+  }
+
+  it('should render first 5 rows in unscrolled container', () => {
+    const calc = new Walkontable.ViewportRowsCalculator(100, 0, 1000, allRows20);
+    const visibleCalc = new Walkontable.ViewportRowsCalculator(100, 0, 1000, allRows20, null, true);
+
+    expect(calc.startRow).toBe(0);
+    expect(calc.startPosition).toBe(0);
+    expect(calc.endRow).toBe(4);
+
+    expect(visibleCalc.startRow).toBe(0);
+    expect(visibleCalc.endRow).toBe(4);
+  });
+
+  it('should render 6 rows, starting from 3 in container scrolled to half of fourth row', () => {
+    const calc = new Walkontable.ViewportRowsCalculator(100, 70, 1000, allRows20);
+    const visibleCalc = new Walkontable.ViewportRowsCalculator(100, 70, 1000, allRows20, null, true);
+
+    expect(calc.startRow).toBe(3);
+    expect(calc.startPosition).toBe(60);
+    expect(calc.endRow).toBe(8);
+
+    expect(visibleCalc.startRow).toBe(4);
+    expect(visibleCalc.endRow).toBe(7);
+  });
+
+  it('should render 10 rows, starting from 1 in container scrolled to half of fourth row (with render overrides)', () => {
+    const overrideFn = function(calc) {
+      calc.startRow -= 2;
+      calc.endRow += 2;
+    };
+    const calc = new Walkontable.ViewportRowsCalculator(100, 70, 1000, allRows20, overrideFn);
+    const visibleCalc = new Walkontable.ViewportRowsCalculator(100, 70, 1000, allRows20, null, true);
+
+    expect(calc.startRow).toBe(1);
+    expect(calc.startPosition).toBe(20);
+    expect(calc.endRow).toBe(10);
+
+    expect(visibleCalc.startRow).toBe(4);
+    expect(visibleCalc.endRow).toBe(7);
+  });
+
+  it('should return number of rendered rows', () => {
+    const calc = new Walkontable.ViewportRowsCalculator(100, 50, 1000, allRows20);
+    const visibleCalc = new Walkontable.ViewportRowsCalculator(100, 50, 1000, allRows20, null, true);
+
+    expect(calc.count).toBe(6);
+
+    expect(visibleCalc.count).toBe(4);
+  });
+
+  it('should render all rows if their size is smaller than viewport', () => {
+    const calc = new Walkontable.ViewportRowsCalculator(200, 0, 8, allRows20);
+    const visibleCalc = new Walkontable.ViewportRowsCalculator(200, 0, 8, allRows20, null, true);
+
+    expect(calc.startRow).toBe(0);
+    expect(calc.endRow).toBe(7);
+    expect(calc.count).toBe(8);
+
+    expect(visibleCalc.startRow).toBe(0);
+    expect(visibleCalc.endRow).toBe(7);
+    expect(visibleCalc.count).toBe(8);
+  });
+
+  it('should render all rows if their size is exactly the viewport', () => {
+    const calc = new Walkontable.ViewportRowsCalculator(200, 0, 10, allRows20);
+    const visibleCalc = new Walkontable.ViewportRowsCalculator(200, 0, 10, allRows20, null, true);
+
+    expect(calc.startRow).toBe(0);
+    expect(calc.endRow).toBe(9);
+    expect(calc.count).toBe(10);
+
+    expect(visibleCalc.startRow).toBe(0);
+    expect(visibleCalc.endRow).toBe(9);
+    expect(visibleCalc.count).toBe(10);
+  });
+
+  it('should render all rows if their size is slightly larger than viewport', () => {
+    const calc = new Walkontable.ViewportRowsCalculator(199, 0, 10, allRows20);
+    const visibleCalc = new Walkontable.ViewportRowsCalculator(199, 0, 10, allRows20, null, true);
+
+    expect(calc.startRow).toBe(0);
+    expect(calc.endRow).toBe(9);
+    expect(calc.count).toBe(10);
+
+    expect(visibleCalc.startRow).toBe(0);
+    expect(visibleCalc.endRow).toBe(8);
+    expect(visibleCalc.count).toBe(9);
+  });
+
+  it('should set null values if total rows is 0', () => {
+    const calc = new Walkontable.ViewportRowsCalculator(200, 0, 0, allRows20);
+    const visibleCalc = new Walkontable.ViewportRowsCalculator(200, 0, 0, allRows20, null, true);
+
+    expect(calc.startRow).toBe(null);
+    expect(calc.startPosition).toBe(null);
+    expect(calc.endRow).toBe(null);
+    expect(calc.count).toBe(0);
+
+    expect(visibleCalc.startRow).toBe(null);
+    expect(visibleCalc.endRow).toBe(null);
+  });
+
+  it('should set null values if total rows is 0 (with overrideFn provided)', () => {
+    const overrideFn = function(myCalc) {
+      myCalc.startRow = 0;
+      myCalc.endRow = 0;
+    };
+    const calc = new Walkontable.ViewportRowsCalculator(200, 0, 0, allRows20, overrideFn);
+    const visibleCalc = new Walkontable.ViewportRowsCalculator(200, 0, 0, allRows20, null, true);
+
+    expect(calc.startRow).toBe(null);
+    expect(calc.startPosition).toBe(null);
+    expect(calc.endRow).toBe(null);
+    expect(calc.count).toBe(0);
+
+    expect(visibleCalc.startRow).toBe(null);
+    expect(visibleCalc.endRow).toBe(null);
+  });
+
+  it('should scroll backwards if total rows is reached', () => {
+    const calc = new Walkontable.ViewportRowsCalculator(190, 350, 20, allRows20);
+    const visibleCalc = new Walkontable.ViewportRowsCalculator(190, 350, 20, allRows20, null, true);
+
+    expect(calc.startRow).toBe(10);
+    expect(calc.startPosition).toBe(200);
+    expect(calc.endRow).toBe(19);
+    expect(calc.count).toBe(10);
+
+    expect(visibleCalc.startRow).toBe(11);
+    expect(visibleCalc.endRow).toBe(19);
+  });
+
+  it('should calculate the number of rows based on a default height, ' +
+    'when the height returned from the function is not a number', () => {
+    const calc = new Walkontable.ViewportRowsCalculator(100, 0, 1000, () => (void 0 + 1));
+    const visibleCalc = new Walkontable.ViewportRowsCalculator(100, 0, 1000, () => (void 0 + 1), null, true);
+
+    expect(calc.startRow).toBe(0);
+    expect(calc.startPosition).toBe(0);
+    expect(calc.endRow).toBe(4);
+
+    expect(visibleCalc.startRow).toBe(0);
+    expect(visibleCalc.endRow).toBe(3);
+  });
+});

+ 73 - 0
handsontable/src/3rdparty/walkontable/test/spec/cell/coords.spec.js

@@ -0,0 +1,73 @@
+describe('Walkontable.CellCoords', () => {
+  describe('isValid', () => {
+
+    const table = document.createElement('table');
+    const wrapper = document.createElement('div');
+    const container = document.createElement('div');
+    wrapper.appendChild(container);
+    container.appendChild(table);
+
+    const wot = new Walkontable.Core({
+      table,
+      data: [],
+      totalRows: 10,
+      totalColumns: 5
+    });
+
+    it('should be false if one of the arguments is smaller than 0', () => {
+      let cellCoords = new Walkontable.CellCoords(-1, 0);
+      let result = cellCoords.isValid(wot);
+      expect(result).toBe(false);
+
+      cellCoords = new Walkontable.CellCoords(0, -1);
+      result = cellCoords.isValid(wot);
+      expect(result).toBe(false);
+    });
+
+    it('should be true if row is within the total number of rows', () => {
+      const cellCoords = new Walkontable.CellCoords(9, 1);
+      const result = cellCoords.isValid(wot);
+      expect(result).toBe(true);
+    });
+
+    it('should be false if row is greater than total number of rows', () => {
+      const cellCoords = new Walkontable.CellCoords(10, 1);
+      const result = cellCoords.isValid(wot);
+      expect(result).toBe(false);
+    });
+
+    it('should be true if column is within the total number of columns', () => {
+      const cellCoords = new Walkontable.CellCoords(1, 4);
+      const result = cellCoords.isValid(wot);
+      expect(result).toBe(true);
+    });
+
+    it('should be false if column is greater than total number of columns', () => {
+      const cellCoords = new Walkontable.CellCoords(1, 5);
+      const result = cellCoords.isValid(wot);
+      expect(result).toBe(false);
+    });
+  });
+
+  describe('isEqual', () => {
+    it('should be equal to itself', () => {
+      const cellCoords = new Walkontable.CellCoords(1, 1);
+      const result = cellCoords.isEqual(cellCoords);
+      expect(result).toBe(true);
+    });
+
+    it('should be equal to another instance with the same row and column', () => {
+      const cellCoords = new Walkontable.CellCoords(1, 1);
+      const cellCoords2 = new Walkontable.CellCoords(1, 1);
+      const result = cellCoords.isEqual(cellCoords2);
+      expect(result).toBe(true);
+    });
+
+    it('should not be equal to an instance with different row or column', () => {
+      const cellCoords = new Walkontable.CellCoords(1, 1);
+      const cellCoords2 = new Walkontable.CellCoords(2, 1);
+      const result = cellCoords.isEqual(cellCoords2);
+      expect(result).toBe(false);
+    });
+  });
+});

Fichier diff supprimé car celui-ci est trop grand
+ 1374 - 0
handsontable/src/3rdparty/walkontable/test/spec/cell/range.spec.js


+ 197 - 0
handsontable/src/3rdparty/walkontable/test/spec/core.spec.js

@@ -0,0 +1,197 @@
+describe('WalkontableCore', () => {
+  let $table;
+  let $container;
+  let $wrapper;
+  const debug = false;
+
+  beforeEach(() => {
+    $wrapper = $('<div></div>').css({ overflow: 'hidden' });
+    $container = $('<div></div>');
+    $table = $('<table></table>'); // create a table that is not attached to document
+    $wrapper.append($container);
+    $container.append($table);
+    $wrapper.appendTo('body');
+    createDataArray(100, 4);
+  });
+
+  afterEach(() => {
+    if (!debug) {
+      $('.wtHolder').remove();
+    }
+
+    $wrapper.remove();
+  });
+
+  it('first row should have the same text as in data source', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+    const TDs = $table.find('tbody tr:first td');
+    expect(TDs[0].innerHTML).toBe('0');
+    expect(TDs[1].innerHTML).toBe('a');
+  });
+
+  it('should bootstrap table if empty TABLE is given', () => {
+    $wrapper.width(200).height(200);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      renderAllRows: true
+    });
+    wt.draw();
+
+    expect($table.find('td').length).toBe(400);
+  });
+
+  it('should bootstrap column headers if THEAD is given', () => {
+    $table.remove();
+    $table = $('<table><thead><tr><th>A</th><th>B</th><th>C</th><th>D</th></tr></thead></table>');
+    $table.appendTo('body');
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }]
+    });
+    wt.draw();
+    expect($table.find('thead th').length).toBe(5); // include corner TH
+    expect($table.find('tbody tr:first th').length).toBe(1);
+    expect($table.find('tbody tr:first td').length).toBe(4);
+  });
+
+  it('should figure out how many columns to display if width param given', () => {
+    $wrapper.width(100);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+    expect($table.find('tbody tr:first td').length).toBe(2);
+  });
+
+  it('should not render table that is removed from DOM', () => {
+    $wrapper.remove();
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+    expect(wt.drawn).toBe(false);
+    expect(wt.drawInterrupted).toBe(true);
+  });
+
+  it('should not render table that is `display: none`', () => {
+    const $div = $('<div style="display: none"></div>').appendTo('body');
+    $div.append($table);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+    expect(wt.drawn).toBe(false);
+    expect(wt.drawInterrupted).toBe(true);
+
+    $div.remove();
+  });
+
+  it('should render empty table (limited height)', () => {
+    createDataArray(0, 5);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+
+    expect(() => {
+      wt.draw(); // second render was giving "Cannot read property 'firstChild' of null" sometimes
+    }).not.toThrow();
+  });
+
+  it('should render empty table (unlimited height)', () => {
+    createDataArray(0, 5);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+
+    expect(() => {
+      wt.draw(); // second render was giving "Cannot read property 'firstChild' of null" sometimes
+    }).not.toThrow();
+  });
+
+  it('should render empty then filled table (limited height)', () => {
+    createDataArray(0, 5);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+    createDataArray(1, 5);
+
+    expect(() => {
+      wt.draw(); // second render was giving "Cannot read property 'firstChild' of null" sometimes
+    }).not.toThrow();
+  });
+
+  it('should render empty then filled table (unlimited height)', () => {
+    createDataArray(0, 5);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+    createDataArray(1, 5);
+
+    expect(() => {
+      wt.draw(); // second render was giving "Cannot read property 'firstChild' of null" sometimes
+    }).not.toThrow();
+  });
+
+  it('should render table with rows but no columns', () => {
+    createDataArray(5, 0);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+
+    expect($table.find('tbody tr').length).toBe(5);
+    expect($table.find('tbody td').length).toBe(0);
+    expect($table.find('tbody col').length).toBe(0);
+  });
+});

+ 547 - 0
handsontable/src/3rdparty/walkontable/test/spec/event.spec.js

@@ -0,0 +1,547 @@
+describe('WalkontableEvent', () => {
+  let $table;
+  const debug = false;
+
+  beforeEach(() => {
+    $table = $('<table></table>'); // create a table that is not attached to document
+    $table.appendTo('body');
+    createDataArray();
+  });
+
+  afterEach(() => {
+    if (!debug) {
+      $('.wtHolder').remove();
+    }
+  });
+
+  it('should call `onCellMouseDown` callback', () => {
+    let myCoords = null;
+    let myTD = null;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      onCellMouseDown(event, coords, TD) {
+        myCoords = coords;
+        myTD = TD;
+      }
+    });
+
+    wt.draw();
+
+    const $td = $table.find('tbody tr:eq(1) td:eq(1)');
+
+    $td.simulate('mousedown');
+
+    expect(myCoords).toEqual(new Walkontable.CellCoords(1, 1));
+    expect(myTD).toEqual($td[0]);
+  });
+
+  it('should call `onCellContextMenu` callback', () => {
+    let myCoords = null;
+    let myTD = null;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      onCellContextMenu(event, coords, TD) {
+        myCoords = coords;
+        myTD = TD;
+      }
+    });
+
+    wt.draw();
+
+    const $td = $table.find('tbody tr:eq(1) td:eq(1)');
+
+    $td.simulate('contextmenu');
+
+    expect(myCoords).toEqual(new Walkontable.CellCoords(1, 1));
+    expect(myTD).toEqual($td[0]);
+  });
+
+  it('should call `onCellMouseOver` callback', () => {
+    let myCoords = null;
+    let myTD = null;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      onCellMouseOver(event, coords, TD) {
+        myCoords = coords;
+        myTD = TD;
+      }
+    });
+
+    wt.draw();
+
+    const $td = $table.find('tbody tr:eq(1) td:eq(1)');
+
+    $td.simulate('mouseover');
+
+    expect(myCoords).toEqual(new Walkontable.CellCoords(1, 1));
+    expect(myTD).toEqual($td[0]);
+  });
+
+  it('should call `onCellMouseOver` callback with correctly passed TD element when cell contains another table', () => {
+    const fn = jasmine.createSpy();
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: [['<table style="width: 50px;"><tr><td class="test">TEST</td></tr></table>']],
+      totalRows: 1,
+      totalColumns: 1,
+      onCellMouseOver: fn,
+      cellRenderer(row, column, TD) {
+        TD.innerHTML = wt.wtSettings.getSetting('data', row, column);
+      },
+    });
+
+    wt.draw();
+
+    const outerTD = $table.find('tbody td:not(td.test)');
+    const innerTD = $table.find('tbody td.test');
+
+    innerTD.simulate('mouseover');
+
+    expect(fn.calls.argsFor(0)[2]).toBe(outerTD[0]);
+  });
+
+  it('should call `onCellMouseOut` callback', () => {
+    let myCoords = null;
+    let myTD = null;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      onCellMouseOut(event, coords, TD) {
+        myCoords = coords;
+        myTD = TD;
+      }
+    });
+
+    wt.draw();
+
+    const $td = $table.find('tbody tr:eq(1) td:eq(1)');
+
+    $td.simulate('mouseover')
+      .simulate('mouseout');
+
+    expect(myCoords).toEqual(new Walkontable.CellCoords(1, 1));
+    expect(myTD).toEqual($td[0]);
+  });
+
+  it('should call `onCellMouseOut` callback with correctly passed TD element when cell contains another table', () => {
+    const fn = jasmine.createSpy();
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: [['<table style="width: 50px;"><tr><td class="test">TEST</td></tr></table>']],
+      totalRows: 1,
+      totalColumns: 1,
+      onCellMouseOut: fn,
+      cellRenderer(row, column, TD) {
+        TD.innerHTML = wt.wtSettings.getSetting('data', row, column);
+      },
+    });
+
+    wt.draw();
+
+    const outerTD = $table.find('tbody td:not(td.test)');
+
+    $table.find('tbody td.test')
+      .simulate('mouseover')
+      .simulate('mouseout');
+
+    expect(fn.calls.argsFor(0)[2]).toBe(outerTD[0]);
+  });
+
+  it('should call `onCellDblClick` callback', () => {
+    let myCoords = null;
+    let myTD = null;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      onCellDblClick(event, coords, TD) {
+        myCoords = coords;
+        myTD = TD;
+      }
+    });
+
+    wt.draw();
+
+    const $td = $table.find('tbody tr:eq(1) td:eq(1)');
+
+    $td.simulate('mousedown')
+      .simulate('mouseup')
+      .simulate('mousedown')
+      .simulate('mouseup');
+
+    expect(myCoords).toEqual(new Walkontable.CellCoords(1, 1));
+    expect(myTD).toEqual($td[0]);
+  });
+
+  it('should call `onCellDblClick` callback, even when it is set only after first click', () => {
+    let myCoords = null;
+    let myTD = null;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+
+    wt.draw();
+
+    const $td = $table.find('tbody tr:eq(1) td:eq(1)');
+
+    $td.simulate('mousedown')
+      .simulate('mouseup')
+      .simulate('mousedown');
+    wt.update('onCellDblClick', (event, coords, TD) => {
+      myCoords = coords;
+      myTD = TD;
+    });
+    $td.simulate('mouseup');
+
+    expect(myCoords).toEqual(new Walkontable.CellCoords(1, 1));
+    expect(myTD).toEqual($td[0]);
+  });
+
+  it('should call `onCellMouseDown` callback when clicked on TH', () => {
+    let called = false;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = col + 1;
+      }],
+      onCellMouseDown() {
+        called = true;
+      }
+    });
+
+    wt.draw();
+
+    $table.find('th:first').simulate('mousedown');
+
+    expect(called).toEqual(true);
+  });
+
+  it('should not call `onCellMouseDown` callback when clicked on the focusable element (column headers)', () => {
+    const opt = ['Maserati', 'Mazda', 'Mercedes', 'Mini', 'Mitsubishi'].map(value => `<option value="${value}">${value}</option>`).join('');
+    let called = false;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = `#${col}<select>${opt}</select>`;
+      }],
+      onCellMouseDown() {
+        called = true;
+      }
+    });
+
+    wt.draw();
+
+    $table.find('th:first select')
+      .focus()
+      .simulate('mousedown');
+
+    expect(called).toBe(false);
+  });
+
+  it('should not call `onCellMouseDown` callback when clicked on the focusable element (cell renderer)', () => {
+    const opt = ['Maserati', 'Mazda', 'Mercedes', 'Mini', 'Mitsubishi'].map(value => `<option value="${value}">${value}</option>`).join('');
+    let called = false;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      cellRenderer(row, column, TD) {
+        TD.innerHTML = `<select>${opt}</select>`;
+      },
+      onCellMouseDown() {
+        called = true;
+      }
+    });
+
+    wt.draw();
+
+    $table.find('td:first select')
+      .focus()
+      .simulate('mousedown');
+
+    expect(called).toBe(false);
+  });
+
+  it('should call `onCellMouseOver` callback when clicked on TH', () => {
+    let called = false;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = col + 1;
+      }],
+      onCellMouseOver(event, coords) {
+        called = coords;
+      }
+    });
+
+    wt.draw();
+
+    $table.find('th:first').simulate('mouseover');
+
+    expect(called.row).toEqual(-1);
+    expect(called.col).toEqual(0);
+  });
+
+  it('should call `onCellDblClick` callback when clicked on TH', () => {
+    let called = false;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = col + 1;
+      }],
+      onCellDblClick() {
+        called = true;
+      }
+    });
+
+    wt.draw();
+
+    $table.find('th:first')
+      .simulate('mousedown')
+      .simulate('mouseup')
+      .simulate('mousedown')
+      .simulate('mouseup');
+
+    expect(called).toEqual(true);
+  });
+
+  it('should not call `onCellDblClick` callback when right-clicked', () => {
+    let called = false;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      onCellDblClick() {
+        called = true;
+      }
+    });
+
+    wt.draw();
+
+    const options = {
+      button: 2
+    };
+
+    $table.find('tbody tr:first td:first')
+      .simulate('mousedown', options)
+      .simulate('mouseup', options)
+      .simulate('mousedown', options)
+      .simulate('mouseup', options);
+
+    expect(called).toEqual(false);
+  });
+
+  it('should not call `onCellDblClick` when first mouse up came from mouse drag', () => {
+    let called = false;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      onCellDblClick() {
+        called = true;
+      }
+    });
+
+    wt.draw();
+
+    $table.find('tbody tr:first td:eq(1)').simulate('mousedown');
+    $table.find('tbody tr:first td:first')
+      .simulate('mouseup')
+      .simulate('mousedown')
+      .simulate('mouseup');
+
+    expect(called).toEqual(false);
+  });
+
+  it('border click should call `onCellMouseDown` callback', () => {
+    let myCoords = null;
+    let myTD = null;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          className: 'current',
+          border: {
+            width: 1,
+            color: 'red',
+            style: 'solid'
+          }
+        })
+      }),
+      onCellMouseDown(event, coords, TD) {
+        myCoords = coords;
+        myTD = TD;
+      }
+    });
+
+    wt.selections.getCell().add(new Walkontable.CellCoords(1, 1));
+    wt.draw();
+
+    const $td = $table.find('tbody tr:eq(1) td:eq(1)');
+
+    $table.parents('.wtHolder')
+      .find('.current:first')
+      .simulate('mousedown');
+
+    expect(myCoords).toEqual(new Walkontable.CellCoords(1, 1));
+    expect(myTD).toEqual($td[0]);
+  });
+
+  it('border click should call `onCellDblClick` callback', () => {
+    let myCoords = null;
+    let myTD = null;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          className: 'current',
+          border: {
+            width: 1,
+            color: 'red',
+            style: 'solid'
+          }
+        })
+      }),
+      onCellDblClick(event, coords, TD) {
+        myCoords = coords;
+        myTD = TD;
+      }
+    });
+
+    wt.selections.getCell().add(new Walkontable.CellCoords(1, 1));
+    wt.draw();
+
+    const $td = $table.find('tbody tr:eq(1) td:eq(1)');
+
+    $table.parents('.wtHolder')
+      .find('.current:first')
+      .simulate('mousedown')
+      .simulate('mouseup')
+      .simulate('mousedown')
+      .simulate('mouseup');
+
+    expect(myCoords).toEqual(new Walkontable.CellCoords(1, 1));
+    expect(myTD).toEqual($td[0]);
+  });
+
+  // corner
+  it('should call `onCellCornerMouseDown` callback', () => {
+    let clicked = false;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          className: 'current',
+          border: {
+            width: 1,
+            color: 'red',
+            style: 'solid'
+          }
+        })
+      }),
+      onCellCornerMouseDown() {
+        clicked = true;
+      }
+    });
+
+    wt.selections.getCell().add(new Walkontable.CellCoords(10, 2));
+    wt.draw();
+
+    $table.parents('.wtHolder')
+      .find('.current.corner')
+      .simulate('mousedown');
+
+    expect(clicked).toEqual(true);
+  });
+
+  it('should call `onCellCornerDblClick` callback, even when it is set only after first click', () => {
+    let clicked = false;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          className: 'current',
+          border: {
+            width: 1,
+            color: 'red',
+            style: 'solid'
+          }
+        })
+      }),
+    });
+
+    wt.selections.getCell().add(new Walkontable.CellCoords(10, 2));
+    wt.draw();
+
+    const $td = $table.parents('.wtHolder').find('.current.corner');
+
+    $td.simulate('mousedown')
+      .simulate('mouseup')
+      .simulate('mousedown');
+    wt.update('onCellCornerDblClick', () => {
+      clicked = true;
+    });
+    $td.simulate('mouseup');
+
+    expect(clicked).toEqual(true);
+  });
+
+  it('should call `onDraw` callback after render', () => {
+    let count = 0;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      onDraw() {
+        count += 1;
+      }
+    });
+
+    wt.draw();
+
+    expect(count).toEqual(1);
+  });
+});

+ 29 - 0
handsontable/src/3rdparty/walkontable/test/spec/filter/column.spec.js

@@ -0,0 +1,29 @@
+describe('Walkontable.ColumnFilter', () => {
+  describe('offsettedTH', () => {
+    it('should do nothing if row header is not visible', () => {
+      const filter = new Walkontable.ColumnFilter();
+      filter.countTH = 0;
+      expect(filter.offsettedTH(1)).toEqual(1);
+    });
+
+    it('should decrease n by 1 if row header is visible', () => {
+      const filter = new Walkontable.ColumnFilter();
+      filter.countTH = 1;
+      expect(filter.offsettedTH(1)).toEqual(0);
+    });
+  });
+
+  describe('unOffsettedTH', () => {
+    it('should do nothing if row header is not visible', () => {
+      const filter = new Walkontable.ColumnFilter();
+      filter.countTH = 0;
+      expect(filter.unOffsettedTH(1)).toEqual(1);
+    });
+
+    it('should increase n by 1 if row header is visible', () => {
+      const filter = new Walkontable.ColumnFilter();
+      filter.countTH = 1;
+      expect(filter.unOffsettedTH(0)).toEqual(1);
+    });
+  });
+});

+ 73 - 0
handsontable/src/3rdparty/walkontable/test/spec/filter/row.spec.js

@@ -0,0 +1,73 @@
+describe('Walkontable.RowFilter', () => {
+
+  describe('offsetted', () => {
+    it('should return n, when offset == 0 && n == 0', () => {
+      const filter = new Walkontable.RowFilter();
+      filter.offset = 0;
+      filter.total = 100;
+      expect(filter.offsetted(0)).toEqual(0);
+    });
+
+    it('should return n, when offset == 0 && n == 5', () => {
+      const filter = new Walkontable.RowFilter();
+      filter.offset = 0;
+      filter.total = 100;
+      expect(filter.offsetted(5)).toEqual(5);
+    });
+
+    it('should return n + 1, when offset == 1 && n == 0', () => {
+      const filter = new Walkontable.RowFilter();
+      filter.offset = 1;
+      filter.total = 100;
+      expect(filter.offsetted(0)).toEqual(1);
+    });
+
+    it('should return n + 5, when offset == 5 && n == 0', () => {
+      const filter = new Walkontable.RowFilter();
+      filter.offset = 5;
+      filter.total = 100;
+      expect(filter.offsetted(0)).toEqual(5);
+    });
+  });
+
+  describe('unOffsetted', () => {
+    it('should return n, when offset == 0 && n == 0', () => {
+      const filter = new Walkontable.RowFilter();
+      filter.offset = 0;
+      filter.total = 100;
+      expect(filter.unOffsetted(0)).toEqual(0);
+    });
+
+    it('should return n, when offset == 0 && n == 5', () => {
+      const filter = new Walkontable.RowFilter();
+      filter.offset = 0;
+      filter.total = 100;
+      expect(filter.unOffsetted(5)).toEqual(5);
+    });
+
+    it('should return n - 1, when offset == 1 && n == 0', () => {
+      const filter = new Walkontable.RowFilter();
+      filter.offset = 1;
+      filter.total = 100;
+      expect(filter.unOffsetted(1)).toEqual(0);
+    });
+
+    it('should return n - 5, when offset == 5 && n == 0', () => {
+      const filter = new Walkontable.RowFilter();
+      filter.offset = 5;
+      filter.total = 100;
+      expect(filter.unOffsetted(5)).toEqual(0);
+    });
+  });
+
+  describe('renderedToSource', () => {
+    it('should translate visible column to source', () => {
+      const filter = new Walkontable.RowFilter();
+      filter.fixedCount = 1; // only cell index 0 falls into this
+      filter.offset = 4;
+      expect(filter.renderedToSource(0)).toEqual(4);
+      expect(filter.renderedToSource(1)).toEqual(5);
+      expect(filter.renderedToSource(2)).toEqual(6);
+    });
+  });
+});

+ 7 - 0
handsontable/src/3rdparty/walkontable/test/spec/index.js

@@ -0,0 +1,7 @@
+[
+  require.context('.', true, /\.spec\.js$/),
+].forEach((req) => {
+  req.keys().forEach((key) => {
+    req(key);
+  });
+});

+ 947 - 0
handsontable/src/3rdparty/walkontable/test/spec/scroll.spec.js

@@ -0,0 +1,947 @@
+describe('WalkontableScroll', () => {
+  const debug = false;
+  let $container;
+  let $wrapper;
+  let $table;
+
+  beforeEach(() => {
+    $wrapper = $('<div></div>').css({ overflow: 'hidden' });
+    $container = $('<div></div>');
+    $table = $('<table></table>'); // create a table that is not attached to document
+    $wrapper.append($container);
+    $container.append($table);
+    $wrapper.appendTo('body');
+    createDataArray(100, 4);
+  });
+
+  afterEach(() => {
+    if (!debug) {
+      $('.wtHolder').remove();
+    }
+
+    $wrapper.remove();
+  });
+
+  describe('scroll', () => {
+    it('should scroll to last column when rowHeaders is not in use', () => {
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+      wt.scrollViewportHorizontally(getTotalColumns() - 1);
+      wt.draw();
+
+      expect($table.find('tbody tr:eq(0) td:last')[0].innerHTML).toBe('c');
+    });
+
+    it('should scroll to last column when rowHeaders is in use', () => {
+      function plusOne(i) {
+        return i + 1;
+      }
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        columnHeaders: [function(col, TH) {
+          TH.innerHTML = plusOne(col);
+        }],
+        rowHeaders: [function(row, TH) {
+          TH.innerHTML = plusOne(row);
+        }]
+      });
+
+      wt.draw();
+      wt.scrollViewportHorizontally(getTotalColumns() - 1);
+      wt.draw();
+
+      expect($table.find('tbody tr:eq(0) td:last')[0].innerHTML).toBe('c');
+    });
+
+    it('scroll not scroll the viewport if all rows are visible', () => {
+      spec().data.splice(5);
+
+      $wrapper.height(201).width(100);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+
+      expect(wt.wtTable.getVisibleRowsCount()).toEqual(5);
+
+      wt.scrollViewportVertically(getTotalRows() - 1);
+      wt.draw();
+
+      expect(wt.wtTable.getCoords($table.find('tbody tr:eq(0) td:eq(0)')[0])).toEqual(new Walkontable.CellCoords(0, 0));
+    });
+
+    it('scroll horizontal should take totalColumns if it is smaller than width', () => {
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+      wt.scrollViewportHorizontally(getTotalColumns() - 1);
+      wt.draw();
+
+      expect(wt.wtTable.getCoords($table.find('tbody tr:eq(0) td:eq(0)')[0])).toEqual(new Walkontable.CellCoords(0, 0));
+    });
+
+    it('scroll vertical should return `false` if given number smaller than 0', () => {
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+
+      expect(wt.scrollViewportVertically(-1)).toBe(false);
+    });
+
+    it('scroll vertical should return `false` if given number bigger than totalRows', () => {
+      spec().data.splice(20, spec().data.length - 20);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+
+      expect(wt.scrollViewportVertically(999)).toBe(false);
+    });
+
+    it('scroll horizontal should return `false` if given number smaller than 0', () => {
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+
+      expect(wt.scrollViewportHorizontally(-1)).toBe(false);
+    });
+
+    it('scroll horizontal should return `false` if given number bigger than totalRows', () => {
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+
+      expect(wt.scrollViewportHorizontally(999)).toBe(false);
+    });
+
+    it('scroll viewport to a cell that is visible should do nothing', () => {
+      $wrapper.height(201).width(120);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+
+      const tmp = wt.getViewport();
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 1));
+      wt.draw();
+
+      expect(wt.getViewport()).toEqual(tmp);
+    });
+
+    it('scroll viewport to a cell on far right should make it visible on right edge', () => {
+      $wrapper.width(125).height(201);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+
+      const height = $wrapper[0].clientHeight;
+      const visibleRowCount = Math.floor(height / 23);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 2));
+      wt.draw();
+
+      expect(wt.getViewport()).toEqual([0, 1, visibleRowCount - 1, 2]);
+    });
+
+    it('scroll viewport to a cell on far left should make it visible on left edge', () => {
+      $wrapper.width(100).height(201);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+
+      const height = $wrapper[0].clientHeight;
+      const visibleRowCount = Math.floor(height / 23);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 3));
+      wt.draw();
+      expect(wt.getViewport()).toEqual([0, 3, visibleRowCount - 1, 3]);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 1));
+      wt.draw();
+      expect(wt.getViewport()).toEqual([0, 1, visibleRowCount - 1, 1]);
+    });
+
+    it('scroll viewport to a cell on far left should make it visible on left edge (with row header)', () => {
+      $wrapper.width(140).height(201);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        rowHeaders: [function(row, TH) {
+          TH.innerHTML = row + 1;
+        }]
+      });
+
+      wt.draw();
+
+      const height = $wrapper[0].clientHeight;
+      const visibleRowCount = Math.floor(height / 23);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 3));
+      wt.draw();
+      expect(wt.getViewport()).toEqual([0, 3, visibleRowCount - 1, 3]);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 1));
+      wt.draw();
+      expect(wt.wtTable.getFirstVisibleColumn()).toEqual(1);
+    });
+
+    it('scroll viewport to a cell on far right should make it visible on right edge (with row header)', () => {
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        rowHeaders: [function(row, TH) {
+          TH.innerHTML = row + 1;
+        }]
+      });
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(0, 2));
+      wt.draw();
+
+      expect(wt.wtTable.getCoords($table.find('tbody tr:first td:last')[0])).toEqual(new Walkontable.CellCoords(0, 3));
+    });
+
+    it('scroll viewport to a cell on far bottom should make it visible on bottom edge', () => {
+      $wrapper.width(125).height(201);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(12, 0));
+      wt.draw();
+
+      expect(wt.getViewport()[0]).toBeAroundValue(5);
+      expect(wt.getViewport()[1]).toBeAroundValue(0);
+      expect(wt.getViewport()[2]).toBeAroundValue(12);
+      expect(wt.getViewport()[3]).toBeAroundValue(1);
+    });
+
+    it('scroll viewport to a cell on far top should make it visible on top edge', () => {
+      $wrapper.width(100).height(201);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(20, 0));
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(12, 0));
+      wt.draw();
+
+      expect(wt.wtTable.getCoords($table.find('tbody tr:first td:first')[0])).toEqual(new Walkontable.CellCoords(12, 0));
+    });
+
+    it('scroll viewport to a cell that does not exist (vertically) should return `false`', () => {
+      spec().data.splice(20, spec().data.length - 20);
+
+      $wrapper.width(100).height(201);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+
+      expect(wt.scrollViewport(new Walkontable.CellCoords(40, 0))).toBe(false);
+    });
+
+    it('scroll viewport to a cell that does not exist (horizontally) should return `false`', () => {
+      $wrapper.width(100).height(201);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+
+      expect(wt.scrollViewport(new Walkontable.CellCoords(0, 40))).toBe(false);
+    });
+
+    it('remove row from the last scroll page should scroll viewport a row up if needed', () => {
+      $wrapper.width(100).height(210);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(getTotalRows() - 1, 0));
+      wt.draw();
+
+      const originalViewportStartRow = wt.getViewport()[0];
+
+      spec().data.splice(getTotalRows() - 4, 1); // remove row at index 96
+      wt.draw();
+
+      expect(originalViewportStartRow - 1).toEqual(wt.getViewport()[0]);
+    });
+
+    it('should scroll to last row if smaller data source is loaded that does not have currently displayed row', () => {
+      $wrapper.width(100).height(260);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+      wt.scrollViewportVertically(50);
+      wt.draw();
+      spec().data.splice(30, spec().data.length - 30);
+      wt.draw();
+
+      expect($table.find('tbody tr').length).toBeGreaterThan(9);
+    });
+
+    it('should scroll to last column if smaller data source is loaded that does not have currently displayed column', () => {
+      createDataArray(20, 100);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+      wt.scrollViewportHorizontally(50);
+      wt.draw();
+      createDataArray(100, 30);
+      wt.draw();
+
+      expect($table.find('tbody tr:first td').length).toBeGreaterThan(3);
+    });
+
+    it('should scroll to last row with very high rows', function() {
+      createDataArray(20, 100);
+
+      for (let i = 0, ilen = this.data.length; i < ilen; i++) {
+        this.data[i][0] += '\n this \nis \na \nmultiline \ncell';
+      }
+
+      $wrapper.width(260).height(201);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+      wt.scrollViewportVertically(getTotalRows() - 1);
+      wt.draw();
+
+      expect($table.find('tbody tr:last td:first')[0]).toBe(wt.wtTable.getCell(new Walkontable.CellCoords(this.data.length - 1, 0))); // last rendered row should be last data row
+    });
+
+    xit('should scroll to last row with very high rows (respecting fixedRows)', () => {
+      createDataArray(20, 100);
+
+      for (let i = 0, ilen = spec().data.length; i < ilen; i++) {
+        spec().data[i][0] += '\n this \nis \na \nmultiline \ncell';
+      }
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        fixedRowsTop: 2
+      });
+
+      wt.draw();
+      wt.scrollViewportVertically(2000);
+      wt.draw();
+
+      expect($table.find('tbody tr:eq(0) td:first')[0]).toBe(wt.wtTable.getCell(new Walkontable.CellCoords(0, 0))); // first rendered row should fixed row 0
+      expect($table.find('tbody tr:eq(1) td:first')[0]).toBe(wt.wtTable.getCell(new Walkontable.CellCoords(1, 0))); // second rendered row should fixed row 1
+      expect($table.find('tbody tr:eq(2) td:first')[0]).toBe(wt.wtTable.getCell(new Walkontable.CellCoords(2, 0))); // third rendered row should fixed row 1
+      expect($table.find('tbody tr:last td:first')[0]).toBe(wt.wtTable.getCell(new Walkontable.CellCoords(spec().data.length - 1, 0))); // last rendered row should be last data row
+    });
+
+    it('should scroll to last column with very wide cells', () => {
+      createDataArray(20, 100);
+      $wrapper.width(260).height(201);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+      wt.scrollViewportHorizontally(50);
+      wt.draw();
+      createDataArray(100, 30);
+      wt.draw();
+
+      expect($table.find('tbody tr:first td').length).toBeGreaterThan(3);
+    });
+
+    it('should scroll the desired cell to the bottom edge even if it\'s located in a fixed column', (done) => {
+      createDataArray(20, 100);
+      $wrapper.width(260).height(201);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        fixedColumnsLeft: 2
+      });
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(8, 1));
+      wt.draw();
+
+      setTimeout(() => {
+        expect(wt.wtTable.getLastVisibleRow()).toBe(8);
+        done();
+      }, 20);
+    });
+
+    it('should update the scroll position of overlays only once, when scrolling the master table', (done) => {
+      createDataArray(100, 100);
+      $wrapper.width(260).height(201);
+
+      const topOverlayCallback = jasmine.createSpy('topOverlayCallback');
+      const leftOverlayCallback = jasmine.createSpy('leftOverlayCallback');
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        fixedColumnsLeft: 2,
+        fixedRowsTop: 2
+      });
+      const masterHolder = wt.wtTable.holder;
+      const leftOverlayHolder = wt.wtOverlays.leftOverlay.clone.wtTable.holder;
+      const topOverlayHolder = wt.wtOverlays.topOverlay.clone.wtTable.holder;
+
+      topOverlayHolder.addEventListener('scroll', topOverlayCallback);
+      leftOverlayHolder.addEventListener('scroll', leftOverlayCallback);
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(50, 50));
+      wt.draw();
+
+      setTimeout(() => {
+        expect(topOverlayCallback.calls.count()).toEqual(1);
+        expect(leftOverlayCallback.calls.count()).toEqual(1);
+
+        expect(topOverlayHolder.scrollLeft).toEqual(masterHolder.scrollLeft);
+        expect(leftOverlayHolder.scrollTop).toEqual(masterHolder.scrollTop);
+
+        topOverlayHolder.removeEventListener('scroll', topOverlayCallback);
+        leftOverlayHolder.removeEventListener('scroll', leftOverlayCallback);
+        done();
+      }, 20);
+    });
+
+    it('should call onScrollVertically hook, if scrollTop was changed', (done) => {
+      createDataArray(100, 100);
+      $wrapper.width(260).height(201);
+
+      const scrollHorizontally = jasmine.createSpy('scrollHorizontal');
+      const scrollVertically = jasmine.createSpy('scrollVertically');
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        fixedColumnsLeft: 2,
+        fixedRowsTop: 2,
+        onScrollVertically: scrollVertically,
+        onScrollHorizontally: scrollHorizontally,
+      });
+
+      wt.draw();
+      wt.wtTable.holder.scrollTop = 400;
+
+      wt.draw();
+
+      setTimeout(() => {
+        expect(scrollVertically.calls.count()).toEqual(1);
+        expect(scrollHorizontally.calls.count()).toEqual(0);
+        done();
+      }, 50);
+    });
+
+    it('should call onScrollHorizontally hook, if scrollLeft was changed', (done) => {
+      createDataArray(100, 100);
+      $wrapper.width(260).height(201);
+
+      const scrollHorizontally = jasmine.createSpy('scrollHorizontal');
+      const scrollVertically = jasmine.createSpy('scrollVertically');
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        fixedColumnsLeft: 2,
+        fixedRowsTop: 2,
+        onScrollVertically: scrollVertically,
+        onScrollHorizontally: scrollHorizontally,
+      });
+
+      wt.draw();
+      wt.wtTable.holder.scrollLeft = 400;
+
+      wt.draw();
+
+      setTimeout(() => {
+        expect(scrollVertically.calls.count()).toEqual(0);
+        expect(scrollHorizontally.calls.count()).toEqual(1);
+
+        done();
+      }, 50);
+    });
+
+    // Commented due to PhantomJS WheelEvent problem.
+    // Throws an error: TypeError: '[object WheelEventConstructor]' is not a constructor
+    xit('should scroll the table when the `wheel` event is triggered on the corner overlay', (done) => {
+      createDataArray(100, 100);
+      $wrapper.width(260).height(201);
+
+      const masterCallback = jasmine.createSpy('masterCallback');
+      const topCallback = jasmine.createSpy('topCallback');
+      const leftCallback = jasmine.createSpy('leftCallback');
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        fixedColumnsLeft: 2,
+        fixedRowsTop: 2
+      });
+
+      wt.draw();
+
+      const topLeftCornerOverlayHolder = wt.wtOverlays.topLeftCornerOverlay.clone.wtTable.holder;
+      const topHolder = wt.wtOverlays.topOverlay.clone.wtTable.holder;
+      const leftHolder = wt.wtOverlays.leftOverlay.clone.wtTable.holder;
+      const masterHolder = wt.wtTable.holder;
+
+      masterHolder.addEventListener('scroll', masterCallback);
+      topHolder.addEventListener('scroll', topCallback);
+      leftHolder.addEventListener('scroll', leftCallback);
+
+      let wheelEvent = new WheelEvent('wheel', {
+        deltaX: 400
+      });
+
+      topLeftCornerOverlayHolder.dispatchEvent(wheelEvent);
+
+      wt.draw();
+
+      setTimeout(() => {
+        expect(masterCallback.callCount).toEqual(1);
+        expect(topCallback.callCount).toEqual(1);
+        expect(leftCallback.callCount).toEqual(0);
+
+        wheelEvent = new WheelEvent('wheel', {
+          deltaY: 400
+        });
+
+        topLeftCornerOverlayHolder.dispatchEvent(wheelEvent);
+        wt.draw();
+      }, 20);
+
+      setTimeout(() => {
+        expect(masterCallback.callCount).toEqual(2);
+        expect(topCallback.callCount).toEqual(1);
+        expect(leftCallback.callCount).toEqual(1);
+        done();
+      }, 40);
+    });
+  });
+
+  describe('scrollViewport - horizontally', () => {
+    beforeEach(() => {
+      $wrapper.width(201).height(201);
+    });
+
+    it('should scroll to last column on the right', () => {
+      spec().data = createSpreadsheetData(10, 10);
+
+      $wrapper.width(201).height(201);
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        columnWidth: 50
+      });
+
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(2);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 9));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(9);
+    });
+
+    it('should not scroll back to a column that is in viewport', () => {
+      spec().data = createSpreadsheetData(10, 10);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        columnWidth: 50
+      });
+
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(2);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 9));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(9);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 9));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(9); // nothing changed
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 8));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(9); // nothing changed
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 7));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(9); // nothing changed
+    });
+
+    it('should scroll back to a column that is before viewport', () => {
+      spec().data = createSpreadsheetData(10, 10);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        columnWidth: 50
+      });
+
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(2);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 9));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(9);
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(0, 3));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(5);
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(0, 4));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(5);// nothing changed
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 9));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(9);
+    });
+
+    it('should scroll to a column that is after viewport', () => {
+      spec().data = createSpreadsheetData(10, 10);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        columnWidth: 50
+      });
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(0, 2));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(2);
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(0, 4));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(4);
+    });
+
+    it('should scroll to a wide column that is after viewport', () => {
+      spec().data = createSpreadsheetData(10, 10);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        columnWidth(col) {
+          if (col === 3) {
+            return 100;
+          }
+
+          return 50;
+        }
+      });
+
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(2);
+      expect(wt.wtTable.getFirstVisibleColumn()).toEqual(0);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 3));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(3);
+      expect(wt.wtTable.getFirstVisibleColumn()).toEqual(2);
+    });
+
+    xit('should scroll to a very wide column that is after viewport', () => {
+      spec().data = createSpreadsheetData(10, 10);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        columnWidth(col) {
+          if (col === 3) {
+            return 300;
+          }
+
+          return 50;
+        }
+      });
+
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(3);
+      expect(wt.wtTable.getFirstVisibleColumn()).toEqual(0);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 3)).draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(3);
+      expect(wt.wtTable.getFirstVisibleColumn()).toEqual(3);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 2)).draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(3);
+      expect(wt.wtTable.getFirstVisibleColumn()).toEqual(2);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 3)).draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(3);
+      expect(wt.wtTable.getFirstVisibleColumn()).toEqual(3);
+
+      wt.scrollViewport(new Walkontable.CellCoords(0, 4)).draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(4);
+      expect(wt.wtTable.getFirstVisibleColumn()).toEqual(3);
+    });
+
+    xit('should scroll to a very wide column that is after viewport (with fixedColumnsLeft)', () => {
+      spec().data = createSpreadsheetData(1, 10);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        columnWidth(col) {
+          if (col === 3) {
+            return 300;
+          }
+
+          return 50;
+        },
+        fixedColumnsLeft: 2
+      });
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(0, 3));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(3);
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(0, 2));
+      wt.draw();
+      expect(wt.wtTable.getFirstVisibleColumn()).toBeGreaterThan(2);
+      expect(wt.wtTable.getLastVisibleColumn()).toBeGreaterThan(2);
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(0, 3));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(3);
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(0, 4));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleColumn()).toEqual(4);
+    });
+  });
+
+  describe('scrollViewport - vertically', () => {
+    beforeEach(() => {
+      $wrapper.width(201).height(201);
+    });
+
+    xit('should scroll to a very high row that is after viewport', () => {
+      spec().data = createSpreadsheetData(20, 1);
+
+      const txt = 'Very very very very very very very very very very very very very very very very very long text.';
+      spec().data[4][0] = txt;
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+      expect(wt.wtTable.getFirstVisibleRow()).toEqual(0);
+
+      wt.scrollViewport(new Walkontable.CellCoords(4, 0));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleRow()).toEqual(4);
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(5, 0));
+      wt.draw();
+      expect(wt.wtTable.getLastVisibleRow()).toEqual(5);
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(4, 0));
+      wt.draw();
+      expect(wt.wtTable.getFirstVisibleRow()).toEqual(4);
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(3, 0));
+      wt.draw();
+      expect(wt.wtTable.getFirstVisibleRow()).toEqual(3);
+    });
+
+    xit('should scroll to a very high row that is after viewport (at the end)', () => {
+      spec().data = createSpreadsheetData(20, 1);
+
+      const txt = 'Very very very very very very very very very very very very very very very very very long text.';
+      spec().data[19][0] = txt;
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(18, 0));
+      wt.draw();
+      expect($table.find('tbody tr').length).toBe(2);
+      expect($table.find('tbody tr:eq(0) td:eq(0)').html()).toBe('A18');
+      expect($table.find('tbody tr:eq(1) td:eq(0)').html()).toBe(txt);
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(19, 0));
+      wt.draw();
+      expect($table.find('tbody tr').length).toBe(1);
+      expect($table.find('tbody tr:eq(0) td:eq(0)').html()).toBe(txt); // scrolled down
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(18, 0));
+      wt.draw();
+      expect($table.find('tbody tr').length).toBe(2);
+      expect($table.find('tbody tr:eq(0) td:eq(0)').html()).toBe('A18'); // scrolled up
+      expect($table.find('tbody tr:eq(1) td:eq(0)').html()).toBe(txt);
+
+      wt.draw();
+      wt.scrollViewport(new Walkontable.CellCoords(17, 0));
+      wt.draw();
+      expect($table.find('tbody tr').length).toBe(3);
+      expect($table.find('tbody tr:eq(0) td:eq(0)').html()).toBe('A17'); // scrolled up
+      expect($table.find('tbody tr:eq(1) td:eq(0)').html()).toBe('A18');
+      expect($table.find('tbody tr:eq(2) td:eq(0)').html()).toBe(txt);
+    });
+  });
+});

+ 56 - 0
handsontable/src/3rdparty/walkontable/test/spec/scrollbar.spec.js

@@ -0,0 +1,56 @@
+describe('WalkontableScrollbar', () => {
+  let $table;
+  let $container;
+  let $wrapper;
+  const debug = false;
+
+  beforeEach(() => {
+    $wrapper = $('<div></div>').css({ overflow: 'hidden' });
+    $container = $('<div></div>');
+    $table = $('<table></table>'); // create a table that is not attached to document
+    $wrapper.append($container);
+    $container.append($table);
+    $wrapper.appendTo('body');
+    createDataArray();
+  });
+
+  afterEach(() => {
+    if (!debug) {
+      $('.wtHolder').remove();
+    }
+    $wrapper.remove();
+  });
+
+  it('should table in DIV.wtHolder that contains 2 scrollbars', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+
+    expect($table.parents('.wtHolder').length).toEqual(1);
+  });
+
+  it('scrolling should have no effect when totalRows is smaller than height', function() {
+    this.data.splice(5, this.data.length - 5);
+
+    try {
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+      wt.draw();
+
+      wt.wtOverlays.topOverlay.onScroll(1);
+      expect(wt.getViewport()[0]).toEqual(0);
+      wt.wtOverlays.topOverlay.onScroll(-1);
+      expect(wt.getViewport()[0]).toEqual(0);
+    } catch (e) {
+      expect(e).toBeUndefined();
+    }
+  });
+});

+ 96 - 0
handsontable/src/3rdparty/walkontable/test/spec/scrollbarNative.spec.js

@@ -0,0 +1,96 @@
+describe('WalkontableScrollbarNative', () => {
+  let $table;
+  let $container;
+  let $wrapper;
+  const debug = false;
+
+  beforeEach(() => {
+    $wrapper = $('<div></div>').css({ overflow: 'hidden' });
+    $wrapper.width(100).height(200);
+    $container = $('<div></div>');
+    $table = $('<table></table>'); // create a table that is not attached to document
+    $wrapper.append($container);
+    $container.append($table);
+    $wrapper.appendTo('body');
+    createDataArray();
+  });
+
+  afterEach(() => {
+    if (!debug) {
+      $('.wtHolder').remove();
+    }
+    $wrapper.remove();
+  });
+
+  it('initial render should be no different than the redraw (vertical)', () => {
+    createDataArray(100, 1);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+
+    const tds = $table.find('td').length;
+    wt.draw();
+
+    expect($table.find('td').length).toEqual(tds);
+  });
+
+  it('initial render should be no different than the redraw (horizontal)', () => {
+    createDataArray(1, 50);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+
+    const tds = $table.find('td').length;
+    wt.draw();
+
+    expect($table.find('td').length).toEqual(tds);
+  });
+
+  it('scrolling 50px down should render 2 more rows', () => {
+    createDataArray(20, 4);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+
+    const lastRenderedRow = wt.wtTable.getLastRenderedRow();
+
+    $(wt.wtTable.holder).scrollTop(50);
+    wt.draw();
+
+    expect(wt.wtTable.getLastRenderedRow()).toEqual(lastRenderedRow + 2);
+  });
+
+  it('should recognize the scrollHandler properly, even if the \'overflow\' property is assigned in an external stylesheet', () => {
+    $wrapper.css({
+      overflow: ''
+    });
+    $wrapper.addClass('testOverflowHidden');
+
+    createDataArray(20, 4);
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+
+    wt.wtOverlays.topOverlay.scrollTo(3);
+    expect($(wt.wtTable.holder).scrollTop()).toEqual(69);
+  });
+});

+ 349 - 0
handsontable/src/3rdparty/walkontable/test/spec/selection.spec.js

@@ -0,0 +1,349 @@
+describe('Walkontable.Selection', () => {
+  let $table;
+  let $container;
+  let $wrapper;
+  const debug = false;
+
+  beforeEach(() => {
+    $wrapper = $('<div></div>').css({ overflow: 'hidden' });
+    $wrapper.width(100).height(200);
+    $container = $('<div></div>');
+    $table = $('<table></table>'); // create a table that is not attached to document
+    $wrapper.append($container);
+    $container.append($table);
+    $wrapper.appendTo('body');
+    createDataArray();
+  });
+
+  afterEach(() => {
+    if (!debug) {
+      $('.wtHolder').remove();
+    }
+    $wrapper.remove();
+  });
+
+  it('should add/remove class to selection when cell is clicked', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController(),
+      onCellMouseDown(event, coords) {
+        wt.selections.getCell().clear();
+        wt.selections.getCell().add(coords);
+        wt.draw();
+      }
+    });
+    wt.draw();
+
+    const $td1 = $table.find('tbody td:eq(0)');
+    const $td2 = $table.find('tbody td:eq(1)');
+    $td1.simulate('mousedown');
+    expect($td1.hasClass('current')).toEqual(true);
+
+    $td2.simulate('mousedown');
+    expect($td1.hasClass('current')).toEqual(false);
+    expect($td2.hasClass('current')).toEqual(true);
+  });
+
+  it('should add class to selection on all overlays', function() {
+    $wrapper.width(300).height(300);
+
+    this.data = createSpreadsheetData(10, 10);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController(),
+      fixedColumnsLeft: 2,
+      fixedRowsTop: 2
+    });
+
+    wt.selections.createOrGetArea().add(new Walkontable.CellCoords(1, 1));
+    wt.selections.createOrGetArea().add(new Walkontable.CellCoords(1, 2));
+    wt.selections.createOrGetArea().add(new Walkontable.CellCoords(2, 1));
+    wt.selections.createOrGetArea().add(new Walkontable.CellCoords(2, 2));
+
+    wt.draw();
+
+    const tds = $wrapper.find('td:contains(B2), td:contains(B3), td:contains(C2), td:contains(C3)');
+    expect(tds.length).toBeGreaterThan(4);
+    for (let i = 0, ilen = tds.length; i < ilen; i++) {
+      expect(tds[i].className).toContain('area');
+    }
+  });
+
+  it('should not add class to selection until it is rerendered', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController(),
+    });
+    wt.draw();
+    wt.selections.getCell().add(new Walkontable.CellCoords(0, 0));
+
+    const $td1 = $table.find('tbody td:eq(0)');
+    expect($td1.hasClass('current')).toEqual(false);
+
+    wt.draw();
+    expect($td1.hasClass('current')).toEqual(true);
+  });
+
+  it('should add/remove border to selection when cell is clicked', (done) => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController(),
+      onCellMouseDown(event, coords) {
+        wt.selections.getCell().clear();
+        wt.selections.getCell().add(coords);
+        wt.draw();
+      }
+    });
+    wt.draw();
+
+    setTimeout(() => {
+      const $td1 = $table.find('tbody tr:eq(1) td:eq(0)');
+      const $td2 = $table.find('tbody tr:eq(2) td:eq(1)');
+      const $top = $(wt.selections.getCell().getBorder(wt).top); // cheat... get border for ht_master
+      $td1.simulate('mousedown');
+
+      const pos1 = $top.position();
+      expect(pos1.top).toBeGreaterThan(0);
+      expect(pos1.left).toBe(0);
+
+      $td2.simulate('mousedown');
+      const pos2 = $top.position();
+
+      expect(pos2.top).toBeGreaterThan(pos1.top);
+      expect(pos2.left).toBeGreaterThan(pos1.left);
+      done();
+    }, 1500);
+  });
+
+  it('should add a selection that is outside of the viewport', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController(),
+    });
+    wt.draw();
+
+    wt.selections.getCell().add([20, 0]);
+    expect(wt.wtTable.getCoords($table.find('tbody tr:first td:first')[0])).toEqual(new Walkontable.CellCoords(0, 0));
+  });
+
+  it('should not scroll the viewport after selection is cleared', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController(),
+    });
+
+    wt.draw();
+
+    wt.selections.getCell().add(new Walkontable.CellCoords(0, 0));
+    wt.draw();
+    expect(wt.wtTable.getFirstVisibleRow()).toEqual(0);
+
+    wt.scrollViewportVertically(17);
+    wt.draw();
+    expect(wt.wtTable.getFirstVisibleRow()).toEqual(10);
+    expect(wt.wtTable.getLastVisibleRow()).toBeAroundValue(17);
+
+    wt.selections.getCell().clear();
+    expect(wt.wtTable.getFirstVisibleRow()).toEqual(10);
+    expect(wt.wtTable.getLastVisibleRow()).toBeAroundValue(17);
+  });
+
+  it('should clear a selection that has more than one cell', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController(),
+    });
+    wt.draw();
+
+    wt.selections.getCell().add(new Walkontable.CellCoords(0, 0));
+    wt.selections.getCell().add(new Walkontable.CellCoords(0, 1));
+    wt.selections.getCell().clear();
+
+    expect(wt.selections.getCell().cellRange).toEqual(null);
+  });
+
+  it('should highlight cells in selected row & column', () => {
+    $wrapper.width(300);
+
+    const customSelection = new Walkontable.Selection({
+      highlightRowClassName: 'highlightRow',
+      highlightColumnClassName: 'highlightColumn'
+    });
+    customSelection.add(new Walkontable.CellCoords(0, 0));
+    customSelection.add(new Walkontable.CellCoords(0, 1));
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController({
+        custom: [customSelection],
+      }),
+    });
+    wt.draw();
+
+    expect($table.find('.highlightRow').length).toEqual(2);
+    expect($table.find('.highlightColumn').length).toEqual((wt.wtTable.getRenderedRowsCount() * 2) - 2);
+  });
+
+  it('should highlight cells in selected row & column, when same class is shared between 2 selection definitions', () => {
+    $wrapper.width(300);
+
+    const customSelection1 = new Walkontable.Selection({
+      highlightRowClassName: 'highlightRow',
+      highlightColumnClassName: 'highlightColumn'
+    });
+
+    customSelection1.add(new Walkontable.CellCoords(0, 0));
+
+    const customSelection2 = new Walkontable.Selection({
+      highlightRowClassName: 'highlightRow',
+      highlightColumnClassName: 'highlightColumn'
+    });
+
+    customSelection2.add(new Walkontable.CellCoords(0, 0));
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController({
+        custom: [customSelection1, customSelection2],
+      }),
+    });
+    wt.draw();
+
+    expect($table.find('.highlightRow').length).toEqual(3);
+    expect($table.find('.highlightColumn').length).toEqual(wt.wtTable.getRenderedRowsCount() - 1);
+  });
+
+  it('should remove highlight when selection is deselected', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          highlightRowClassName: 'highlightRow',
+          highlightColumnClassName: 'highlightColumn'
+        }),
+      }),
+    });
+    wt.draw();
+
+    wt.selections.getCell().add(new Walkontable.CellCoords(0, 0));
+    wt.selections.getCell().add(new Walkontable.CellCoords(0, 1));
+    wt.draw();
+
+    wt.selections.getCell().clear();
+    wt.draw();
+
+    expect($table.find('.highlightRow').length).toEqual(0);
+    expect($table.find('.highlightColumn').length).toEqual(0);
+  });
+
+  it('should add/remove appropriate class to the row/column headers of selected cells', () => {
+    $wrapper.width(300);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }],
+      columnHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }],
+      selections: createSelectionController({
+        current: new Walkontable.Selection({
+          highlightRowClassName: 'highlightRow',
+          highlightColumnClassName: 'highlightColumn'
+        }),
+      }),
+    });
+    wt.draw();
+
+    wt.selections.getCell().add(new Walkontable.CellCoords(1, 1));
+    wt.selections.getCell().add(new Walkontable.CellCoords(2, 2));
+    wt.draw();
+
+    // left side:
+    // -2 -> because one row is partially visible
+
+    // right side:
+    // *2 -> because there are 2 columns selected
+    // +2 -> because there are the headers
+    // -4 -> because 4 cells are selected = there are overlapping highlightRow class
+    expect($table.find('.highlightRow').length).toEqual((wt.wtViewport.columnsVisibleCalculator.count * 2) + 2 - 4);
+    expect($table.find('.highlightColumn').length - 2).toEqual((wt.wtViewport.rowsVisibleCalculator.count * 2) + 2 - 4);
+    expect($table.find('.highlightColumn').length).toEqual(14);
+    expect(getTableTopClone().find('.highlightColumn').length).toEqual(2);
+    expect(getTableTopClone().find('.highlightRow').length).toEqual(0);
+    expect(getTableLeftClone().find('.highlightColumn').length).toEqual(0);
+    expect(getTableLeftClone().find('.highlightRow').length).toEqual(2);
+
+    const $colHeaders = $table.find('thead tr:first-child th');
+    const $rowHeaders = $table.find('tbody tr th:first-child');
+
+    expect($colHeaders.eq(2).hasClass('highlightColumn')).toBe(true);
+    expect($colHeaders.eq(3).hasClass('highlightColumn')).toBe(true);
+
+    expect($rowHeaders.eq(1).hasClass('highlightRow')).toBe(true);
+    expect($rowHeaders.eq(2).hasClass('highlightRow')).toBe(true);
+
+    wt.selections.getCell().clear();
+    wt.draw();
+
+    expect($table.find('.highlightRow').length).toEqual(0);
+    expect($table.find('.highlightColumn').length).toEqual(0);
+    expect(getTableTopClone().find('.highlightColumn').length).toEqual(0);
+    expect(getTableTopClone().find('.highlightRow').length).toEqual(0);
+    expect(getTableLeftClone().find('.highlightColumn').length).toEqual(0);
+    expect(getTableLeftClone().find('.highlightRow').length).toEqual(0);
+  });
+
+  describe('replace', () => {
+    it('should replace range from property and return true', () => {
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        selections: createSelectionController(),
+      });
+      wt.selections.getCell().add(new Walkontable.CellCoords(1, 1));
+      wt.selections.getCell().add(new Walkontable.CellCoords(3, 3));
+      const result = wt.selections.getCell().replace(new Walkontable.CellCoords(3, 3), new Walkontable.CellCoords(4, 4));
+
+      expect(result).toBe(true);
+      expect(wt.selections.getCell().getCorners()).toEqual([1, 1, 4, 4]);
+    });
+  });
+});

+ 127 - 0
handsontable/src/3rdparty/walkontable/test/spec/settings/columnHeaders.spec.js

@@ -0,0 +1,127 @@
+describe('columnHeaders option', () => {
+  let $table;
+  let $container;
+  let $wrapper;
+  const debug = false;
+
+  beforeEach(() => {
+    $wrapper = $('<div></div>').css({ overflow: 'hidden', position: 'relative' });
+    $wrapper.width(500).height(201);
+    $container = $('<div></div>');
+    $table = $('<table></table>'); // create a table that is not attached to document
+    $wrapper.append($container);
+    $container.append($table);
+    $wrapper.appendTo('body');
+    createDataArray();
+  });
+
+  afterEach(() => {
+    if (!debug) {
+      $('.wtHolder').remove();
+    }
+    $wrapper.remove();
+  });
+
+  it('should not add class `htColumnHeaders` when column headers are disabled', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+
+    expect($wrapper.hasClass('htColumnHeaders')).toBe(false);
+  });
+
+  it('should add class `htColumnHeaders` when column headers are enabled', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = col + 1;
+      }]
+    });
+    wt.draw();
+
+    expect($wrapper.hasClass('htColumnHeaders')).toBe(true);
+  });
+
+  it('should create table with column headers', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = col + 1;
+      }]
+    });
+    wt.draw();
+
+    expect($wrapper.find('.ht_clone_left colgroup col').length).toBe(0);
+    expect($wrapper.find('.ht_clone_left thead tr').length).toBe(1);
+    expect($wrapper.find('.ht_clone_left tbody tr').length).toBe(0);
+    expect($wrapper.find('.ht_clone_top colgroup col').length).toBe(4);
+    expect($wrapper.find('.ht_clone_top thead tr').length).toBe(1);
+    expect($wrapper.find('.ht_clone_top tbody tr').length).toBe(0);
+    expect($wrapper.find('.ht_master colgroup col').length).toBe(4);
+    expect($wrapper.find('.ht_master thead tr').length).toBe(1);
+    expect($wrapper.find('.ht_master tbody tr').length).toBe(9);
+  });
+
+  it('should create column headers with correct height when th has css `white-space: normal`', () => {
+    const style = $('<style>.handsontable thead th {white-space: normal;}</style>').appendTo('head');
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = 'Client State State';
+      }],
+      columnWidth: 80
+    });
+    wt.draw();
+
+    expect($wrapper.find('.ht_clone_top thead tr').height()).toBe(43);
+    style.remove();
+  });
+
+  it('should create column headers with correct height when th has css `white-space: pre-line` (default)', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = 'Client State State';
+      }],
+      columnWidth: 80
+    });
+    wt.draw();
+
+    expect($wrapper.find('.ht_clone_top thead tr').height()).toBe(23);
+  });
+
+  it('should generate column headers from function', () => {
+    const headers = ['Description', 2012, 2013, 2014];
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      columnHeaders: [function(column, TH) {
+        TH.innerHTML = headers[column];
+      }]
+    });
+    wt.draw();
+
+    const visibleHeaders = headers.slice(0, wt.wtTable.getLastRenderedColumn() + 1); // headers for rendered columns only
+
+    expect($table.find('thead tr:first th').length).toBe(visibleHeaders.length);
+    expect($table.find('thead tr:first th').text()).toEqual(visibleHeaders.join(''));
+  });
+});

+ 59 - 0
handsontable/src/3rdparty/walkontable/test/spec/settings/preventOverflow.spec.js

@@ -0,0 +1,59 @@
+describe('preventOverflow option', () => {
+  let $table;
+  let $container;
+  let $wrapper;
+  const debug = false;
+
+  beforeEach(() => {
+    $wrapper = $('<div></div>').css({ position: 'relative' });
+    $wrapper.width(500).height(201);
+    $container = $('<div></div>');
+    $table = $('<table></table>'); // create a table that is not attached to document
+    $wrapper.append($container);
+    $container.append($table);
+    $wrapper.appendTo('body');
+    createDataArray(100, 100);
+  });
+
+  afterEach(() => {
+    if (!debug) {
+      $('.wtHolder').remove();
+    }
+    $wrapper.remove();
+  });
+
+  it('should set overflow to `auto` for master table when `horizontal` value is passed', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      preventOverflow() {
+        return 'horizontal';
+      }
+    });
+    wt.draw();
+
+    expect($table.parents('.wtHolder').css('overflow')).toBe('auto');
+    expect($table.parents('.ht_master').css('overflow')).toBe('visible');
+  });
+
+  it('should set overflow-x to `auto` for top clone when `horizontal` value is passed', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      columnHeaders: [function(column, TH) {
+        TH.innerHTML = column + 1;
+      }],
+      preventOverflow() {
+        return 'horizontal';
+      }
+    });
+    wt.draw();
+
+    expect($(wt.wtTable.wtRootElement.parentNode).find('.ht_clone_top .wtHolder').css('overflow-x')).toBe('auto');
+    expect($(wt.wtTable.wtRootElement.parentNode).find('.ht_clone_top .wtHolder').css('overflow-y')).toBe('hidden');
+  });
+});

+ 111 - 0
handsontable/src/3rdparty/walkontable/test/spec/settings/rowHeaders.spec.js

@@ -0,0 +1,111 @@
+describe('rowHeaders option', () => {
+  let $table;
+  let $container;
+  let $wrapper;
+  const debug = false;
+
+  beforeEach(() => {
+    $wrapper = $('<div></div>').css({ overflow: 'hidden', position: 'relative' });
+    $wrapper.width(500).height(201);
+    $container = $('<div></div>');
+    $table = $('<table></table>'); // create a table that is not attached to document
+    $wrapper.append($container);
+    $container.append($table);
+    $wrapper.appendTo('body');
+    createDataArray();
+  });
+
+  afterEach(() => {
+    if (!debug) {
+      $('.wtHolder').remove();
+    }
+    $wrapper.remove();
+  });
+
+  it('should not add class `htRowHeader` when row headers are disabled', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+
+    expect($wrapper.hasClass('htRowHeaders')).toBe(false);
+  });
+
+  it('should add class `htRowHeader` when row headers are enabled', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }]
+    });
+    wt.draw();
+
+    expect($wrapper.hasClass('htRowHeaders')).toBe(true);
+  });
+
+  it('should create table row headers', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }]
+    });
+    wt.draw();
+
+    expect($wrapper.find('.ht_clone_left colgroup col').length).toBe(1);
+    expect($wrapper.find('.ht_clone_left thead tr').length).toBe(0);
+    expect($wrapper.find('.ht_clone_left tbody tr').length).toBe(9);
+    expect($wrapper.find('.ht_clone_top colgroup col').length).toBe(0);
+    expect($wrapper.find('.ht_clone_top thead tr').length).toBe(0);
+    expect($wrapper.find('.ht_clone_top tbody tr').length).toBe(0);
+    expect($wrapper.find('.ht_master colgroup col').length).toBe(5);
+    expect($wrapper.find('.ht_master thead tr').length).toBe(0);
+    expect($wrapper.find('.ht_master tbody tr').length).toBe(9);
+  });
+
+  it('should generate headers from function', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }]
+    });
+
+    wt.draw();
+    const potentialRowCount = 9;
+    expect($table.find('tbody td').length).toBe(potentialRowCount * wt.wtTable.getRenderedColumnsCount()); // displayed cells
+    expect($table.find('tbody th').length).toBe(potentialRowCount); // 9*1=9 displayed row headers
+    expect($table.find('tbody tr:first th').length).toBe(1); // only one th per row
+    expect($table.find('tbody tr:first th')[0].innerHTML).toBe('1'); // this should be the first row header
+  });
+
+  it('should add \'rowHeader\' class to row header column', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }],
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = col + 1;
+      }]
+    });
+    wt.draw();
+
+    expect($table.find('col:first').hasClass('rowHeader')).toBe(true);
+  });
+});

+ 205 - 0
handsontable/src/3rdparty/walkontable/test/spec/settings/stretchH.spec.js

@@ -0,0 +1,205 @@
+describe('stretchH option', () => {
+  let $table;
+  let $container;
+  let $wrapper;
+  const debug = false;
+
+  beforeEach(() => {
+    $wrapper = $('<div></div>').css({ overflow: 'hidden', position: 'relative' });
+    $wrapper.width(500).height(201);
+    $container = $('<div></div>');
+    $table = $('<table></table>'); // create a table that is not attached to document
+    $wrapper.append($container);
+    $container.append($table);
+    $wrapper.appendTo('body');
+    createDataArray();
+  });
+
+  afterEach(() => {
+    if (!debug) {
+      $('.wtHolder').remove();
+    }
+    $wrapper.remove();
+  });
+
+  it('should stretch all visible columns when stretchH equals \'all\'', () => {
+    createDataArray(20, 2);
+
+    $wrapper.width(500).height(400);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      stretchH: 'all',
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }]
+    });
+    wt.draw();
+
+    expect($table.outerWidth()).toBeAroundValue(wt.wtTable.holder.clientWidth);
+    // fix differences between Mac and Linux PhantomJS
+    expect($table.find('col:eq(2)').width() - $table.find('col:eq(1)').width()).toBeInArray([-1, 0, 1]);
+  });
+
+  it('should stretch all visible columns when stretchH equals \'all\' and window is resized', (done) => {
+    createDataArray(20, 2);
+
+    $wrapper.width(500).height(400);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      stretchH: 'all',
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }]
+    });
+    wt.draw();
+
+    const initialTableWidth = $table.outerWidth();
+    expect(initialTableWidth).toBeAroundValue($table[0].clientWidth);
+
+    $wrapper.width(600).height(500);
+
+    const evt = document.createEvent('CustomEvent'); // MUST be 'CustomEvent'
+    evt.initCustomEvent('resize', false, false, null);
+    window.dispatchEvent(evt);
+
+    setTimeout(() => {
+      const currentTableWidth = $table.outerWidth();
+      expect(currentTableWidth).toBeAroundValue($table[0].clientWidth);
+      expect(currentTableWidth).toBeGreaterThan(initialTableWidth);
+      done();
+    }, 10);
+  });
+
+  it('should stretch all visible columns when stretchH equals \'all\' (when rows are of variable height)', function() {
+    createDataArray(20, 2);
+
+    for (let i = 0, ilen = this.data.length; i < ilen; i++) {
+      if (i % 2) {
+        this.data[i][0] += ' this is a cell that contains a lot of text, which will make it multi-line';
+      }
+    }
+
+    $wrapper.width(300);
+    $wrapper.css({
+      overflow: 'hidden'
+    });
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      stretchH: 'all'
+    });
+    wt.draw();
+
+    let expectedColWidth = ((300 - getScrollbarWidth()) / 2);
+    expectedColWidth = Math.floor(expectedColWidth);
+
+    const wtHider = $table.parents('.wtHider');
+    expect(wtHider.find('col:eq(0)').width()).toBeAroundValue(expectedColWidth);
+    expect(wtHider.find('col:eq(1)').width() - expectedColWidth).toBeInArray([0, 1]); // fix differences between Mac and Linux PhantomJS
+  });
+
+  it('should stretch last visible column when stretchH equals \'last\' (vertical scroll)', () => {
+    createDataArray(20, 2);
+
+    $wrapper.width(300).height(201);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      stretchH: 'last',
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }]
+    });
+    wt.draw();
+
+    const wtHider = $table.parents('.wtHider');
+    expect(wtHider.outerWidth()).toBe(getTableWidth($table));
+    expect(wtHider.find('col:eq(1)').width()).toBeLessThan(wtHider.find('col:eq(2)').width());
+  });
+
+  it('should stretch last column when stretchH equals \'last\' (horizontal scroll)', () => {
+    createDataArray(5, 20);
+
+    $wrapper.width(400).height(201);
+    spec().data[0][19] = 'longer text';
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      stretchH: 'last',
+      columnHeaders: [function(index, TH) {
+        TH.innerHTML = index + 1;
+      }],
+      columnWidth(index) {
+        return index === 19 ? 100 : 50;
+      }
+    });
+
+    wt.draw();
+    wt.scrollViewportHorizontally(19);
+    wt.draw();
+
+    const wtHider = $table.parents('.wtHider');
+
+    expect(wtHider.find('col:eq(6)').width()).toBe(100);
+  });
+
+  it('should stretch last visible column when stretchH equals \'last\' (no scrolls)', () => {
+    createDataArray(2, 2);
+
+    $wrapper.width(300).height(201);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      stretchH: 'last',
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }]
+    });
+    wt.draw();
+
+    const wtHider = $table.parents('.wtHider');
+    expect(wtHider.outerWidth()).toBe(getTableWidth($table));
+    expect(wtHider.find('col:eq(1)').width()).toBeLessThan(wtHider.find('col:eq(2)').width());
+  });
+
+  it('should not stretch when stretchH equals \'none\'', () => {
+    createDataArray(20, 2);
+    $wrapper.width(300).height(201);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      stretchH: 'none',
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }]
+    });
+    wt.draw();
+
+    expect($table.width()).toBeLessThan($wrapper.width());
+    expect($table.find('col:eq(1)').width()).toBe($table.find('col:eq(2)').width());
+  });
+
+});

+ 593 - 0
handsontable/src/3rdparty/walkontable/test/spec/table.spec.js

@@ -0,0 +1,593 @@
+describe('WalkontableTable', () => {
+  let $table;
+  let $container;
+  let $wrapper;
+  const debug = false;
+
+  beforeEach(() => {
+    $wrapper = $('<div></div>').css({ overflow: 'hidden', position: 'relative' });
+    $wrapper.width(100).height(201);
+    $container = $('<div></div>');
+    $table = $('<table></table>'); // create a table that is not attached to document
+    $wrapper.append($container);
+    $container.append($table);
+    $wrapper.appendTo('body');
+    createDataArray();
+  });
+
+  afterEach(() => {
+    if (!debug) {
+      $('.wtHolder').remove();
+    }
+
+    $wrapper.remove();
+  });
+
+  it('should create as many rows as fits in height', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+    expect($table.find('tbody tr').length).toBe(9);
+  });
+
+  it('should create as many rows as in `totalRows` if it is smaller than `height`', function() {
+    this.data.splice(5, this.data.length - 5);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+    expect($table.find('tbody tr').length).toBe(5);
+  });
+
+  it('first row should have as many columns as in THEAD', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = col + 1;
+      }]
+    });
+    wt.draw();
+    expect($table.find('tbody tr:first td').length).toBe($table.find('thead th').length);
+  });
+
+  it('should put a blank cell in the corner if both rowHeaders and colHeaders are set', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      columnHeaders: [
+        function(col, TH) {
+          if (col > -1) {
+            TH.innerHTML = 'Column';
+          }
+        }
+      ],
+      rowHeaders: [
+        function(row, TH) {
+          if (row > -1) {
+            TH.innerHTML = 'Row';
+          }
+        }
+      ]
+    });
+    wt.draw();
+    expect($table.find('thead tr:first th').length).toBe(wt.wtTable.getRenderedColumnsCount() + 1); // 4 columns in THEAD + 1 empty cell in the corner
+    expect($table.find('thead tr:first th:eq(0)')[0].innerHTML.replace(/&nbsp;/, '')).toBe(''); // corner row is empty (or contains only &nbsp;)
+    expect($table.find('thead tr:first th:eq(1)')[0].innerHTML).toBe('Column');
+    expect($table.find('tbody tr:first th:eq(0)')[0].innerHTML).toBe('Row');
+  });
+
+  it('getCell should only return cells from rendered rows', function() {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+
+    expect(wt.wtTable.getCell(new Walkontable.CellCoords(7, 0)) instanceof HTMLElement).toBe(true);
+    expect($table.find('tr:eq(8) td:first-child').text()).toEqual(this.data[8][0].toString());
+    expect(wt.wtTable.getCell(new Walkontable.CellCoords(20, 0))).toBe(-2); // exit code
+    expect(wt.wtTable.getCell(new Walkontable.CellCoords(25, 0))).toBe(-2); // exit code
+  });
+
+  it('getCoords should return coords of TD', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+
+    const $td2 = $table.find('tbody tr:eq(1) td:eq(1)');
+    expect(wt.wtTable.getCoords($td2[0])).toEqual(new Walkontable.CellCoords(1, 1));
+  });
+
+  it('getCoords should return coords of TD (with row header)', () => {
+
+    $wrapper.width(300);
+
+    function plusOne(i) {
+      return i + 1;
+    }
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = plusOne(row);
+      }]
+    });
+    wt.draw();
+
+    const $td2 = $table.find('tbody tr:eq(1) td:eq(1)');
+    expect(wt.wtTable.getCoords($td2[0])).toEqual(new Walkontable.CellCoords(1, 1));
+  });
+
+  it('getStretchedColumnWidth should return valid column width when stretchH is set as \'all\'', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }],
+      stretchH: 'all'
+    });
+    wt.draw();
+    wt.wtViewport.columnsRenderCalculator.refreshStretching(502);
+
+    expect(wt.wtTable.getStretchedColumnWidth(0, 50)).toBe(125);
+    expect(wt.wtTable.getStretchedColumnWidth(1, 50)).toBe(125);
+    expect(wt.wtTable.getStretchedColumnWidth(2, 50)).toBe(125);
+    expect(wt.wtTable.getStretchedColumnWidth(3, 50)).toBe(127);
+  });
+
+  it('getStretchedColumnWidth should return valid column width when stretchH is set as \'last\'', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }],
+      stretchH: 'last'
+    });
+    wt.draw();
+    wt.wtViewport.columnsRenderCalculator.refreshStretching(502);
+
+    expect(wt.wtTable.getStretchedColumnWidth(0, 50)).toBe(50);
+    expect(wt.wtTable.getStretchedColumnWidth(1, 50)).toBe(50);
+    expect(wt.wtTable.getStretchedColumnWidth(2, 50)).toBe(50);
+    expect(wt.wtTable.getStretchedColumnWidth(3, 50)).toBe(352);
+  });
+
+  it('should use custom cell renderer if provided', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      cellRenderer(row, column, TD) {
+        const cellData = getData(row, column);
+
+        if (cellData === void 0) {
+          TD.innerHTML = '';
+        } else {
+          TD.innerHTML = cellData;
+        }
+        TD.className = '';
+        TD.style.backgroundColor = 'yellow';
+      }
+    });
+    wt.draw();
+    expect($table.find('td:first')[0].style.backgroundColor).toBe('yellow');
+  });
+
+  it('should remove rows if they were removed in data source', function() {
+    this.data.splice(8, this.data.length - 8); // second param is required by IE8
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+    expect($table.find('tbody tr').length).toBe(8);
+
+    this.data.splice(7, this.data.length - 7); // second param is required by IE8
+    wt.draw();
+    expect($table.find('tbody tr').length).toBe(7);
+  });
+
+  it('should render as much columns as the container width allows, if width is null', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = col + 1;
+      }]
+    });
+    wt.draw();
+    expect($table.find('thead tr:first').children().length).toBe(2);
+    expect($table.find('tbody tr:first').children().length).toBe(2);
+
+    $wrapper.width(200);
+    wt.draw();
+    expect($table.find('thead tr:first').children().length).toBe(4);
+    expect($table.find('tbody tr:first').children().length).toBe(4);
+  });
+
+  it('should render as much columns as the container width allows, if width is null (with row header)', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }],
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = col + 1;
+      }]
+    });
+    wt.draw();
+    expect($table.find('thead tr:first').children().length).toBe(2);
+    expect($table.find('tbody tr:first').children().length).toBe(2);
+
+    $wrapper.width(200);
+    wt.draw();
+    expect($table.find('thead tr:first').children().length).toBe(4);
+    expect($table.find('tbody tr:first').children().length).toBe(4);
+  });
+
+  it('should use column width function to get column width', () => {
+
+    $wrapper.width(600);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }],
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = col + 1;
+      }],
+      columnWidth(column) {
+        return (column + 1) * 50;
+      }
+    });
+    wt.draw();
+    expect($table.find('tbody tr:first td:eq(0)').outerWidth()).toBe(50);
+    expect($table.find('tbody tr:first td:eq(1)').outerWidth()).toBe(100);
+    expect($table.find('tbody tr:first td:eq(2)').outerWidth()).toBe(150);
+    expect($table.find('tbody tr:first td:eq(3)').outerWidth()).toBe(200);
+  });
+
+  it('should use column width array to get column width', () => {
+
+    $wrapper.width(600);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }],
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = col + 1;
+      }],
+      columnWidth: [50, 100, 150, 201]
+    });
+    wt.draw();
+    expect($table.find('tbody tr:first td:eq(0)').outerWidth()).toBe(50);
+    expect($table.find('tbody tr:first td:eq(1)').outerWidth()).toBe(100);
+    expect($table.find('tbody tr:first td:eq(2)').outerWidth()).toBe(150);
+    expect($table.find('tbody tr:first td:eq(3)').outerWidth()).toBe(201);
+  });
+
+  it('should use column width integer to get column width', () => {
+
+    $wrapper.width(600);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }],
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = col + 1;
+      }],
+      columnWidth: 100
+    });
+    wt.draw();
+    expect($table.find('tbody tr:first td:eq(0)').outerWidth()).toBe(100);
+    expect($table.find('tbody tr:first td:eq(1)').outerWidth()).toBe(100);
+    expect($table.find('tbody tr:first td:eq(2)').outerWidth()).toBe(100);
+    expect($table.find('tbody tr:first td:eq(3)').outerWidth()).toBe(100);
+  });
+
+  it('should use column width also when there are no rows', function() {
+    this.data.length = 0;
+
+    $wrapper.width(600);
+
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: 4,
+      rowHeaders: [function(row, TH) {
+        TH.innerHTML = row + 1;
+      }],
+      columnHeaders: [function(col, TH) {
+        TH.innerHTML = col + 1;
+      }],
+      columnWidth: 100
+    });
+    wt.draw();
+    // start from eq(1) because eq(0) is corner header
+    expect($table.find('thead tr:first th:eq(1)').outerWidth()).toBe(100);
+    expect($table.find('thead tr:first th:eq(2)').outerWidth()).toBe(100);
+    expect($table.find('thead tr:first th:eq(3)').outerWidth()).toBe(100);
+    expect($table.find('thead tr:first th:eq(4)').outerWidth()).toBe(100);
+  });
+
+  it('should render a cell that is outside of the viewport horizontally', () => {
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns
+    });
+    wt.draw();
+    $table.find('tbody td').html('');
+    wt.draw();
+    expect($table.find('tbody tr:first td').length).toBe(2);
+  });
+
+  it('should not render a cell when fastDraw == true', () => {
+    let count = 0;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      cellRenderer(row, column, TD) {
+        count += 1;
+        return wt.wtSettings.defaults.cellRenderer(row, column, TD);
+      }
+    });
+    wt.draw();
+    const oldCount = count;
+    wt.draw(true);
+    expect(count).toBe(oldCount);
+  });
+
+  it('should not ignore fastDraw == true when grid was scrolled by amount of rows that doesn\'t exceed endRow', () => {
+    let count = 0;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      cellRenderer(row, column, TD) {
+        count += 1;
+        return wt.wtSettings.defaults.cellRenderer(row, column, TD);
+      },
+      viewportRowCalculatorOverride(calc) {
+        calc.endRow += 10;
+      }
+    });
+    wt.draw();
+    const oldCount = count;
+
+    wt.scrollViewportVertically(8);
+    wt.draw(true);
+    expect(count).not.toBeGreaterThan(oldCount);
+  });
+
+  it('should ignore fastDraw == true when grid was scrolled by amount of rows that exceeds endRow', () => {
+    let count = 0;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      cellRenderer(row, column, TD) {
+        count += 1;
+        return wt.wtSettings.defaults.cellRenderer(row, column, TD);
+      },
+      viewportRowCalculatorOverride(calc) {
+        calc.endRow += 10;
+      }
+    });
+    wt.draw();
+    const oldCount = count;
+
+    wt.scrollViewportVertically(10);
+    wt.draw(true);
+    expect(count).not.toBeGreaterThan(oldCount);
+
+    wt.scrollViewportVertically(getTotalRows() - 1);
+    wt.draw(true);
+    expect(count).toBeGreaterThan(oldCount);
+  });
+
+  it('should not ignore fastDraw == true when grid was scrolled by amount of columns that doesn\'t exceed endColumn', () => {
+    createDataArray(50, 50);
+    let count = 0;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      cellRenderer(row, column, TD) {
+        count += 1;
+        return wt.wtSettings.defaults.cellRenderer(row, column, TD);
+      },
+      viewportColumnCalculatorOverride(calc) {
+        calc.endColumn += 10;
+      }
+    });
+    wt.draw();
+    const oldCount = count;
+
+    wt.scrollViewportHorizontally(8);
+    wt.draw(true);
+
+    expect(count).not.toBeGreaterThan(oldCount);
+  });
+
+  it('should ignore fastDraw == true when grid was scrolled by amount of columns that exceeds endColumn', () => {
+    createDataArray(50, 50);
+    let count = 0;
+    const wt = new Walkontable.Core({
+      table: $table[0],
+      data: getData,
+      totalRows: getTotalRows,
+      totalColumns: getTotalColumns,
+      cellRenderer(row, column, TD) {
+        count += 1;
+
+        return wt.wtSettings.defaults.cellRenderer(row, column, TD);
+      },
+      viewportColumnCalculatorOverride(calc) {
+        calc.endColumn += 10;
+      }
+    });
+    wt.draw();
+    const oldCount = count;
+
+    wt.scrollViewportHorizontally(10);
+    wt.draw(true);
+    expect(count).not.toBeGreaterThan(oldCount);
+
+    wt.scrollViewportHorizontally(11);
+    wt.draw(true);
+    expect(count).toBeGreaterThan(oldCount);
+  });
+
+  describe('cell header border', () => {
+    it('should be correct visible in fixedColumns and without row header', () => {
+      createDataArray(50, 50);
+      $wrapper.width(500).height(400);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns,
+        columnWidth: 70,
+        fixedColumnsLeft: 2,
+        columnHeaders: [function() {}]
+      });
+      wt.draw();
+
+      expect($('.ht_clone_top_left_corner thead tr th').eq(0).css('border-left-width')).toBe('1px');
+      expect($('.ht_clone_top_left_corner thead tr th').eq(0).css('border-right-width')).toBe('1px');
+      expect($('.ht_clone_top_left_corner thead tr th').eq(1).css('border-left-width')).toBe('0px');
+      expect($('.ht_clone_top_left_corner thead tr th').eq(1).css('border-right-width')).toBe('1px');
+    });
+  });
+
+  describe('isLastRowFullyVisible', () => {
+    it('should be false because it is only partially visible', () => {
+      createDataArray(8, 4);
+
+      $wrapper.width(185).height(175);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+      wt.draw();
+
+      expect(wt.wtTable.isLastRowFullyVisible()).toEqual(false);
+    });
+
+    it('should be true because it is fully visible', () => {
+      createDataArray(8, 4);
+
+      $wrapper.width(185).height(185);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+      wt.draw();
+      wt.scrollViewportVertically(7);
+      wt.draw();
+
+      expect(wt.wtTable.isLastRowFullyVisible()).toEqual(true);
+    });
+  });
+
+  xdescribe('isLastColumnFullyVisible', () => {
+    it('should be false because it is only partially visible', () => {
+      createDataArray(18, 4);
+
+      $wrapper.width(209).height(185);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+      wt.draw();
+
+      expect(wt.wtTable.isLastColumnFullyVisible()).toEqual(false); // few pixels are obstacled by scrollbar
+    });
+
+    it('should be true because it is fully visible', () => {
+      createDataArray(18, 4);
+
+      $wrapper.width(180).height(185);
+
+      const wt = new Walkontable.Core({
+        table: $table[0],
+        data: getData,
+        totalRows: getTotalRows,
+        totalColumns: getTotalColumns
+      });
+      wt.draw();
+      wt.scrollHorizontal(1);
+
+      expect(wt.wtTable.isLastColumnFullyVisible()).toEqual(true);
+    });
+  });
+});

+ 11 - 0
handsontable/src/cellTypes/autocompleteType.js

@@ -0,0 +1,11 @@
+import { getEditor } from './../editors';
+import { getRenderer } from './../renderers';
+import { getValidator } from './../validators';
+
+const CELL_TYPE = 'autocomplete';
+
+export default {
+  editor: getEditor(CELL_TYPE),
+  renderer: getRenderer(CELL_TYPE),
+  validator: getValidator(CELL_TYPE),
+};

+ 9 - 0
handsontable/src/cellTypes/checkboxType.js

@@ -0,0 +1,9 @@
+import { getEditor } from './../editors';
+import { getRenderer } from './../renderers';
+
+const CELL_TYPE = 'checkbox';
+
+export default {
+  editor: getEditor(CELL_TYPE),
+  renderer: getRenderer(CELL_TYPE),
+};

+ 12 - 0
handsontable/src/cellTypes/dateType.js

@@ -0,0 +1,12 @@
+import { getEditor } from './../editors';
+import { getRenderer } from './../renderers';
+import { getValidator } from './../validators';
+
+const CELL_TYPE = 'date';
+
+export default {
+  editor: getEditor(CELL_TYPE),
+  // displays small gray arrow on right side of the cell
+  renderer: getRenderer('autocomplete'),
+  validator: getValidator(CELL_TYPE),
+};

+ 12 - 0
handsontable/src/cellTypes/dropdownType.js

@@ -0,0 +1,12 @@
+import { getEditor } from './../editors';
+import { getRenderer } from './../renderers';
+import { getValidator } from './../validators';
+
+const CELL_TYPE = 'dropdown';
+
+export default {
+  editor: getEditor(CELL_TYPE),
+  // displays small gray arrow on right side of the cell
+  renderer: getRenderer('autocomplete'),
+  validator: getValidator('autocomplete'),
+};

+ 10 - 0
handsontable/src/cellTypes/handsontableType.js

@@ -0,0 +1,10 @@
+import { getEditor } from './../editors';
+import { getRenderer } from './../renderers';
+
+const CELL_TYPE = 'handsontable';
+
+export default {
+  editor: getEditor(CELL_TYPE),
+  // displays small gray arrow on right side of the cell
+  renderer: getRenderer('autocomplete'),
+};

+ 77 - 0
handsontable/src/cellTypes/index.js

@@ -0,0 +1,77 @@
+import staticRegister from './../utils/staticRegister';
+import { registerEditor } from './../editors';
+import { registerRenderer } from './../renderers';
+import { registerValidator } from './../validators';
+
+import autocompleteCellType from './autocompleteType';
+import checkboxCellType from './checkboxType';
+import dateCellType from './dateType';
+import dropdownCellType from './dropdownType';
+import handsontableCellType from './handsontableType';
+import numericCellType from './numericType';
+import passwordCellType from './passwordType';
+import textCellType from './textType';
+import timeCellType from './timeType';
+
+const {
+  register,
+  getItem,
+  hasItem,
+  getNames,
+  getValues,
+} = staticRegister('cellTypes');
+
+_register('autocomplete', autocompleteCellType);
+_register('checkbox', checkboxCellType);
+_register('date', dateCellType);
+_register('dropdown', dropdownCellType);
+_register('handsontable', handsontableCellType);
+_register('numeric', numericCellType);
+_register('password', passwordCellType);
+_register('text', textCellType);
+_register('time', timeCellType);
+
+/**
+ * Retrieve cell type object.
+ *
+ * @param {String} name Cell type identification.
+ * @returns {Object} Returns cell type object.
+ */
+function _getItem(name) {
+  if (!hasItem(name)) {
+    throw Error(`You declared cell type "${name}" as a string that is not mapped to a known object.
+                 Cell type must be an object or a string mapped to an object registered by "Handsontable.cellTypes.registerCellType" method`);
+  }
+
+  return getItem(name);
+}
+
+/**
+ * Register cell type under specified name.
+ *
+ * @param {String} name Cell type identification.
+ * @param {Object} type An object with contains keys (eq: `editor`, `renderer`, `validator`) which describes specified behaviour of the cell.
+ */
+function _register(name, type) {
+  const { editor, renderer, validator } = type;
+
+  if (editor) {
+    registerEditor(name, editor);
+  }
+  if (renderer) {
+    registerRenderer(name, renderer);
+  }
+  if (validator) {
+    registerValidator(name, validator);
+  }
+
+  register(name, type);
+}
+
+export {
+  _register as registerCellType,
+  _getItem as getCellType,
+  hasItem as hasCellType,
+  getNames as getRegisteredCellTypeNames,
+  getValues as getRegisteredCellTypes,
+};

+ 12 - 0
handsontable/src/cellTypes/numericType.js

@@ -0,0 +1,12 @@
+import { getEditor } from './../editors';
+import { getRenderer } from './../renderers';
+import { getValidator } from './../validators';
+
+const CELL_TYPE = 'numeric';
+
+export default {
+  editor: getEditor(CELL_TYPE),
+  renderer: getRenderer(CELL_TYPE),
+  validator: getValidator(CELL_TYPE),
+  dataType: 'number',
+};

+ 10 - 0
handsontable/src/cellTypes/passwordType.js

@@ -0,0 +1,10 @@
+import { getEditor } from './../editors';
+import { getRenderer } from './../renderers';
+
+const CELL_TYPE = 'password';
+
+export default {
+  editor: getEditor(CELL_TYPE),
+  renderer: getRenderer(CELL_TYPE),
+  copyable: false,
+};

+ 0 - 0
handsontable/src/cellTypes/textType.js


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff