Selaa lähdekoodia

Merge branch 'master' of http://192.168.1.41:3000/SmartCost/SCCommon

vian 5 vuotta sitten
vanhempi
commit
9572b7ffc9

+ 10 - 2
handsontable/handsontable.d.ts

@@ -196,8 +196,7 @@ declare namespace _Handsontable {
 
     isDestroyed: boolean;
 
-    // 自定义的字段,用于挂载原始数据
-    originSourceData?: any | any[];
+    rootElement: HTMLElement;
   }
 }
 
@@ -2149,6 +2148,8 @@ declare namespace Handsontable {
     visibleRows?: number;
     width?: number | (() => void);
     wordWrap?: boolean;
+    // 自定义添加
+    prop?: string;
   }
 
   interface Hooks {
@@ -2303,6 +2304,7 @@ declare namespace Handsontable {
     skipLengthCache?: (delay: number) => void;
     unmodifyCol?: (col: number) => void;
     unmodifyRow?: (row: number) => void;
+    destroy?: (instance?: Handsontable) => void;
   }
 
   namespace I18n {
@@ -2356,6 +2358,9 @@ declare namespace Handsontable {
     NumericRenderer: renderers.Numeric;
     PasswordRenderer: renderers.Password;
     TextRenderer: renderers.Text;
+
+    // 自定义添加
+    registerRenderer: (rendererName: string, rendererFunc: Handsontable.renderers.Base) => void;
   }
 
   interface Helper {
@@ -2775,6 +2780,9 @@ declare class Handsontable extends _Handsontable.Core {
   static plugins: Handsontable.Plugins;
 
   static renderers: Handsontable.Renderers;
+
+  // 自定义添加
+  rendererCache?: any;
 }
 
 export = Handsontable;

+ 1 - 1
handsontable/package.json

@@ -10,7 +10,7 @@
     "url": "https://github.com/handsontable/handsontable/issues"
   },
   "author": "Handsoncode <hello@handsontable.com>",
-  "version": "6.2.5",
+  "version": "6.2.9",
   "browser": "dist/handsontable.js",
   "main": "commonjs/index.js",
   "module": "es/index.js",

+ 1 - 0
handsontable/src/editors/dateEditor.js

@@ -74,6 +74,7 @@ class DateEditor extends TextEditor {
    */
   destroyElements() {
     this.$datePicker.destroy();
+    this.datePicker.remove();
   }
 
   /**

+ 1 - 0
handsontable/src/plugins/contextMenu/menu.js

@@ -251,6 +251,7 @@ class Menu {
     this.close();
     this.parentMenu = null;
     this.eventManager.destroy();
+    this.container.remove();
   }
 
   /**

+ 100 - 0
handsontable/src/plugins/groupingHeaders/groupingHeaders.js

@@ -0,0 +1,100 @@
+// You need to import the BasePlugin class in order to inherit from it.
+import BasePlugin from './../_base';
+import { registerPlugin } from './../../plugins';
+
+/**
+ * @plugin InternalPluginSkeleton
+ * Note: keep in mind, that Handsontable instance creates one instance of the plugin class.
+ *
+ * @description
+ * Blank plugin template. It needs to inherit from the BasePlugin class.
+ */
+class GroupingHeaders extends BasePlugin {
+
+  // The argument passed to the constructor is the currently processed Handsontable instance object.
+  constructor(hotInstance) {
+    super(hotInstance);
+
+    // Initialize all your public properties in the class' constructor.
+    /**
+     * yourProperty description.
+     *
+     * @type {String}
+     */
+    this.yourProperty = '';
+    /**
+     * anotherProperty description.
+     * @type {Array}
+     */
+    this.anotherProperty = [];
+  }
+
+  /**
+   * Checks if the plugin is enabled in the settings.
+   */
+  isEnabled() {
+    return !!this.hot.getSettings().internalPluginSkeleton;
+  }
+
+  /**
+   * The enablePlugin method is triggered on the beforeInit hook. It should contain your initial plugin setup, along with
+   * the hook connections.
+   * Note, that this method is run only if the statement in the isEnabled method is true.
+   */
+  enablePlugin() {
+    this.yourProperty = 'Your Value';
+
+    // Add all your plugin hooks here. It's a good idea to make use of the arrow functions to keep the context consistent.
+    this.addHook('afterChange', (changes, source) => this.onAfterChange(changes, source));
+
+    // The super method assigns the this.enabled property to true, which can be later used to check if plugin is already enabled.
+    super.enablePlugin();
+  }
+
+  /**
+   * The disablePlugin method is used to disable the plugin. Reset all of your classes properties to their default values here.
+   */
+  disablePlugin() {
+    this.yourProperty = '';
+    this.anotherProperty = [];
+
+    // The super method takes care of clearing the hook connections and assigning the 'false' value to the 'this.enabled' property.
+    super.disablePlugin();
+  }
+
+  /**
+   * The updatePlugin method is called on the afterUpdateSettings hook (unless the updateSettings method turned the plugin off).
+   * It should contain all the stuff your plugin needs to do to work properly after the Handsontable instance settings were modified.
+   */
+  updatePlugin() {
+
+    // The updatePlugin method needs to contain all the code needed to properly re-enable the plugin. In most cases simply disabling and enabling the plugin should do the trick.
+    this.disablePlugin();
+    this.enablePlugin();
+
+    super.updatePlugin();
+  }
+
+  /**
+   * The afterChange hook callback.
+   *
+   * @param {Array} changes Array of changes.
+   * @param {String} source Describes the source of the change.
+   */
+  onAfterChange(changes, source) {
+    // afterChange callback goes here.
+  }
+
+  /**
+   * The destroy method should de-assign all of your properties.
+   */
+  destroy() {
+    // The super method takes care of de-assigning the event callbacks, plugin hooks and clearing all the plugin properties.
+    super.destroy();
+  }
+}
+
+export default GroupingHeaders;
+
+// You need to register your plugin in order to use it within Handsontable.
+registerPlugin('internalPluginSkeleton', GroupingHeaders);

+ 2 - 2
handsontable/src/plugins/index.js

@@ -19,7 +19,7 @@ import ObserveChanges from './observeChanges/observeChanges';
 import Search from './search/search';
 import TouchScroll from './touchScroll/touchScroll';
 import UndoRedo from './undoRedo/undoRedo';
-import TrimRows from './trimRows/trimRows';
+import NestedHeaders from './nestedHeaders/nestedHeaders';
 import Base from './_base';
 
 export {
@@ -43,7 +43,7 @@ export {
   PersistentState,
   Search,
   TouchScroll,
-  TrimRows,
   UndoRedo,
+  NestedHeaders,
   Base,
 };

+ 3 - 0
handsontable/src/plugins/nestedHeaders/nestedHeaders.css

@@ -0,0 +1,3 @@
+.handsontable thead th.hiddenHeader:not(:first-of-type) {
+  display: none;
+}

+ 679 - 0
handsontable/src/plugins/nestedHeaders/nestedHeaders.js

@@ -0,0 +1,679 @@
+import {
+  addClass,
+  removeClass,
+  fastInnerHTML,
+  empty,
+} from '../../helpers/dom/element';
+import { rangeEach } from '../../helpers/number';
+import { arrayEach } from '../../helpers/array';
+import { objectEach } from '../../helpers/object';
+import { toSingleLine } from '../../helpers/templateLiteralTag';
+import { warn } from '../../helpers/console';
+import { registerPlugin } from '../../plugins';
+import BasePlugin from '../_base';
+import { CellCoords } from '../../3rdparty/walkontable/src';
+import GhostTable from './utils/ghostTable';
+
+import './nestedHeaders.css';
+
+/**
+ * @plugin NestedHeaders
+ * @pro
+ *
+ * @description
+ * The plugin allows to create a nested header structure, using the HTML's colspan attribute.
+ *
+ * To make any header wider (covering multiple table columns), it's corresponding configuration array element should be
+ * provided as an object with `label` and `colspan` properties. The `label` property defines the header's label,
+ * while the `colspan` property defines a number of columns that the header should cover.
+ *
+ * __Note__ that the plugin supports a *nested* structure, which means, any header cannot be wider than it's "parent". In
+ * other words, headers cannot overlap each other.
+ * @example
+ *
+ * ```js
+ * const container = document.getElementById('example');
+ * const hot = new Handsontable(container, {
+ *   date: getData(),
+ *   nestedHeaders: [
+ *           ['A', {label: 'B', colspan: 8}, 'C'],
+ *           ['D', {label: 'E', colspan: 4}, {label: 'F', colspan: 4}, 'G'],
+ *           ['H', {label: 'I', colspan: 2}, {label: 'J', colspan: 2}, {label: 'K', colspan: 2}, {label: 'L', colspan: 2}, 'M'],
+ *           ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W']
+ *  ],
+ * ```
+ */
+class NestedHeaders extends BasePlugin {
+
+  constructor(hotInstance) {
+    super(hotInstance);
+    /**
+     * Nested headers cached settings.
+     *
+     * @private
+     * @type {Object}
+     */
+    this.settings = [];
+    /**
+     * Cached number of column header levels.
+     *
+     * @private
+     * @type {Number}
+     */
+    this.columnHeaderLevelCount = 0;
+    /**
+     * Array of nested headers' colspans.
+     *
+     * @private
+     * @type {Array}
+     */
+    this.colspanArray = [];
+    /**
+     * Custom helper for getting widths of the nested headers.
+     * @TODO This should be changed after refactor handsontable/utils/ghostTable.
+     *
+     * @private
+     * @type {GhostTable}
+     */
+    this.ghostTable = new GhostTable(this);
+  }
+
+  /**
+   * Check if plugin is enabled
+   *
+   * @returns {Boolean}
+   */
+  isEnabled() {
+    return !!this.hot.getSettings().nestedHeaders;
+  }
+
+  /**
+   * Enables the plugin functionality for this Handsontable instance.
+   */
+  enablePlugin() {
+    if (this.enabled) {
+      return;
+    }
+
+    this.settings = this.hot.getSettings().nestedHeaders;
+
+    this.addHook('afterGetColumnHeaderRenderers', array => this.onAfterGetColumnHeaderRenderers(array));
+    this.addHook('afterInit', () => this.onAfterInit());
+    this.addHook('afterOnCellMouseDown', (event, coords) => this.onAfterOnCellMouseDown(event, coords));
+    this.addHook('beforeOnCellMouseOver', (event, coords, TD, blockCalculations) => this.onBeforeOnCellMouseOver(event, coords, TD, blockCalculations));
+    this.addHook('afterViewportColumnCalculatorOverride', calc => this.onAfterViewportColumnCalculatorOverride(calc));
+    this.addHook('modifyColWidth', (width, column) => this.onModifyColWidth(width, column));
+
+    this.setupColspanArray();
+    this.checkForFixedColumnsCollision();
+
+    this.columnHeaderLevelCount = this.hot.view ? this.hot.view.wt.getSetting('columnHeaders').length : 0;
+
+    super.enablePlugin();
+  }
+
+  /**
+   * Disables the plugin functionality for this Handsontable instance.
+   */
+  disablePlugin() {
+    this.clearColspans();
+
+    this.settings = [];
+    this.columnHeaderLevelCount = 0;
+    this.colspanArray = [];
+
+    this.ghostTable.clear();
+
+    super.disablePlugin();
+  }
+
+  /**
+   * Updates the plugin state. This method is executed when {@link Core#updateSettings} is invoked.
+   */
+  updatePlugin() {
+    this.disablePlugin();
+    this.enablePlugin();
+
+    super.updatePlugin();
+    this.ghostTable.buildWidthsMapper();
+  }
+
+  /**
+   * Clear the colspans remaining after plugin usage.
+   *
+   * @private
+   */
+  clearColspans() {
+    if (!this.hot.view) {
+      return;
+    }
+
+    const headerLevels = this.hot.view.wt.getSetting('columnHeaders').length;
+    const mainHeaders = this.hot.view.wt.wtTable.THEAD;
+    const topHeaders = this.hot.view.wt.wtOverlays.topOverlay.clone.wtTable.THEAD;
+    const topLeftCornerHeaders = this.hot.view.wt.wtOverlays.topLeftCornerOverlay ?
+      this.hot.view.wt.wtOverlays.topLeftCornerOverlay.clone.wtTable.THEAD : null;
+
+    for (let i = 0; i < headerLevels; i++) {
+      const masterLevel = mainHeaders.childNodes[i];
+
+      if (!masterLevel) {
+        break;
+      }
+
+      const topLevel = topHeaders.childNodes[i];
+      const topLeftCornerLevel = topLeftCornerHeaders ? topLeftCornerHeaders.childNodes[i] : null;
+
+      for (let j = 0, masterNodes = masterLevel.childNodes.length; j < masterNodes; j++) {
+        masterLevel.childNodes[j].removeAttribute('colspan');
+
+        if (topLevel && topLevel.childNodes[j]) {
+          topLevel.childNodes[j].removeAttribute('colspan');
+        }
+
+        if (topLeftCornerHeaders && topLeftCornerLevel && topLeftCornerLevel.childNodes[j]) {
+          topLeftCornerLevel.childNodes[j].removeAttribute('colspan');
+        }
+      }
+    }
+  }
+
+  /**
+   * Check if the nested headers overlap the fixed columns overlay, if so - display a warning.
+   *
+   * @private
+   */
+  checkForFixedColumnsCollision() {
+    const fixedColumnsLeft = this.hot.getSettings().fixedColumnsLeft;
+
+    arrayEach(this.colspanArray, (value, i) => {
+      if (this.getNestedParent(i, fixedColumnsLeft) !== fixedColumnsLeft) {
+        warn(toSingleLine`You have declared a Nested Header overlapping the Fixed Columns section - it may lead to visual
+          glitches. To prevent that kind of problems, split the nested headers between the fixed and non-fixed columns.`);
+      }
+    });
+  }
+
+  /**
+   * Check if the configuration contains overlapping headers.
+   *
+   * @private
+   */
+  checkForOverlappingHeaders() {
+    arrayEach(this.colspanArray, (level, i) => {
+      arrayEach(this.colspanArray[i], (header, j) => {
+        if (header.colspan > 1) {
+          const row = this.levelToRowCoords(i);
+          const childHeaders = this.getChildHeaders(row, j);
+
+          if (childHeaders.length > 0) {
+            let childColspanSum = 0;
+
+            arrayEach(childHeaders, (col) => {
+              childColspanSum += this.getColspan(row + 1, col);
+            });
+
+            if (childColspanSum > header.colspan) {
+              warn(toSingleLine`Your Nested Headers plugin setup contains overlapping headers. This kind of configuration
+                is currently not supported and might result in glitches.`);
+            }
+
+            return false;
+          }
+        }
+      });
+    });
+  }
+
+  /**
+   * Create an internal array containing information of the headers with a colspan attribute.
+   *
+   * @private
+   */
+  setupColspanArray() {
+    function checkIfExists(array, index) {
+      if (!array[index]) {
+        array[index] = [];
+      }
+    }
+
+    objectEach(this.settings, (levelValues, level) => {
+      objectEach(levelValues, (val, col, levelValue) => {
+        checkIfExists(this.colspanArray, level);
+
+        if (levelValue[col].colspan === void 0) {
+          this.colspanArray[level].push({
+            label: levelValue[col] || '',
+            colspan: 1,
+            hidden: false
+          });
+
+        } else {
+          const colspan = levelValue[col].colspan || 1;
+
+          this.colspanArray[level].push({
+            label: levelValue[col].label || '',
+            colspan,
+            hidden: false
+          });
+
+          this.fillColspanArrayWithDummies(colspan, level);
+        }
+      });
+    });
+  }
+
+  /**
+   * Fill the "colspan array" with default data for the dummy hidden headers.
+   *
+   * @private
+   * @param {Number} colspan The colspan value.
+   * @param {Number} level Header level.
+   */
+  fillColspanArrayWithDummies(colspan, level) {
+    rangeEach(0, colspan - 2, () => {
+      this.colspanArray[level].push({
+        label: '',
+        colspan: 1,
+        hidden: true,
+      });
+    });
+  }
+
+  /**
+   * Generates the appropriate header renderer for a header row.
+   *
+   * @private
+   * @param {Number} headerRow The header row.
+   * @returns {Function}
+   *
+   * @fires Hooks#afterGetColHeader
+   */
+  headerRendererFactory(headerRow) {
+    const _this = this;
+
+    return function(index, TH) {
+      TH.removeAttribute('colspan');
+      removeClass(TH, 'hiddenHeader');
+
+      // header row is the index of header row counting from the top (=> positive values)
+      if (_this.colspanArray[headerRow][index] && _this.colspanArray[headerRow][index].colspan) {
+        const colspan = _this.colspanArray[headerRow][index].colspan;
+        const fixedColumnsLeft = _this.hot.getSettings().fixedColumnsLeft || 0;
+        const topLeftCornerOverlay = _this.hot.view.wt.wtOverlays.topLeftCornerOverlay;
+        const leftOverlay = _this.hot.view.wt.wtOverlays.leftOverlay;
+        const isInTopLeftCornerOverlay = topLeftCornerOverlay ? topLeftCornerOverlay.clone.wtTable.THEAD.contains(TH) : false;
+        const isInLeftOverlay = leftOverlay ? leftOverlay.clone.wtTable.THEAD.contains(TH) : false;
+
+        if (colspan > 1) {
+          TH.setAttribute('colspan', isInTopLeftCornerOverlay || isInLeftOverlay ? Math.min(colspan, fixedColumnsLeft - index) : colspan);
+        }
+
+        if (isInTopLeftCornerOverlay || isInLeftOverlay && index === fixedColumnsLeft - 1) {
+          addClass(TH, 'overlayEdge');
+        }
+      }
+
+      if (_this.colspanArray[headerRow][index] && _this.colspanArray[headerRow][index].hidden) {
+        addClass(TH, 'hiddenHeader');
+      }
+
+      empty(TH);
+
+      const divEl = document.createElement('DIV');
+      addClass(divEl, 'relative');
+      const spanEl = document.createElement('SPAN');
+      addClass(spanEl, 'colHeader');
+
+      fastInnerHTML(spanEl, _this.colspanArray[headerRow][index] ? _this.colspanArray[headerRow][index].label || '' : '');
+
+      divEl.appendChild(spanEl);
+
+      TH.appendChild(divEl);
+
+      _this.hot.runHooks('afterGetColHeader', index, TH);
+    };
+  }
+
+  /**
+   * Returns the colspan for the provided coordinates.
+   *
+   * @private
+   * @param {Number} row Row index.
+   * @param {Number} column Column index.
+   * @returns {Number}
+   */
+  getColspan(row, column) {
+    const header = this.colspanArray[this.rowCoordsToLevel(row)][column];
+
+    return header ? header.colspan : 1;
+  }
+
+  /**
+   * Translates the level value (header row index from the top) to the row value (negative index).
+   *
+   * @private
+   * @param {Number} level Header level.
+   * @returns {Number}
+   */
+  levelToRowCoords(level) {
+    return level - this.columnHeaderLevelCount;
+  }
+
+  /**
+   * Translates the row value (negative index) to the level value (header row index from the top).
+   *
+   * @private
+   * @param {Number} row Row index.
+   * @returns {Number}
+   */
+  rowCoordsToLevel(row) {
+    return row + this.columnHeaderLevelCount;
+  }
+
+  /**
+   * Returns the column index of the "parent" nested header.
+   *
+   * @private
+   * @param {Number} level Header level.
+   * @param {Number} column Column index.
+   * @returns {*}
+   */
+  getNestedParent(level, column) {
+    if (level < 0) {
+      return false;
+    }
+
+    const colspan = this.colspanArray[level][column] ? this.colspanArray[level][column].colspan : 1;
+    const hidden = this.colspanArray[level][column] ? this.colspanArray[level][column].hidden : false;
+
+    if (colspan > 1 || (colspan === 1 && hidden === false)) {
+      return column;
+
+    }
+    let parentCol = column - 1;
+
+    do {
+      if (this.colspanArray[level][parentCol].colspan > 1) {
+        break;
+      }
+
+      parentCol -= 1;
+    } while (column >= 0);
+
+    return parentCol;
+  }
+
+  /**
+   * Returns (physical) indexes of headers below the header with provided coordinates.
+   *
+   * @private
+   * @param {Number} row Row index.
+   * @param {Number} column Column index.
+   * @returns {Number[]}
+   */
+  getChildHeaders(row, column) {
+    const level = this.rowCoordsToLevel(row);
+    const childColspanLevel = this.colspanArray[level + 1];
+    const nestedParentCol = this.getNestedParent(level, column);
+    let colspan = this.colspanArray[level][column].colspan;
+    const childHeaderRange = [];
+
+    if (!childColspanLevel) {
+      return childHeaderRange;
+    }
+
+    rangeEach(nestedParentCol, nestedParentCol + colspan - 1, (i) => {
+      if (childColspanLevel[i] && childColspanLevel[i].colspan > 1) {
+        colspan -= childColspanLevel[i].colspan - 1;
+      }
+
+      if (childColspanLevel[i] && !childColspanLevel[i].hidden && childHeaderRange.indexOf(i) === -1) {
+        childHeaderRange.push(i);
+      }
+    });
+
+    return childHeaderRange;
+  }
+
+  /**
+   * Fill the remaining colspanArray entries for the undeclared column headers.
+   *
+   * @private
+   */
+  fillTheRemainingColspans() {
+    objectEach(this.settings, (levelValue, level) => {
+      rangeEach(this.colspanArray[level].length - 1, this.hot.countCols() - 1, (col) => {
+        this.colspanArray[level].push({
+          label: levelValue[col] || '',
+          colspan: 1,
+          hidden: false
+        });
+
+      }, true);
+    });
+  }
+
+  /**
+   * Updates headers highlight in nested structure.
+   *
+   * @private
+   */
+  updateHeadersHighlight() {
+    const selection = this.hot.getSelectedLast();
+
+    if (selection === void 0) {
+      return;
+    }
+
+    const wtOverlays = this.hot.view.wt.wtOverlays;
+    const selectionByHeader = this.hot.selection.isSelectedByColumnHeader();
+    const from = Math.min(selection[1], selection[3]);
+    const to = Math.max(selection[1], selection[3]);
+    const levelLimit = selectionByHeader ? -1 : this.columnHeaderLevelCount - 1;
+
+    const changes = [];
+    const classNameModifier = className => (TH, modifier) => () => modifier(TH, className);
+    const highlightHeader = classNameModifier('ht__highlight');
+    const activeHeader = classNameModifier('ht__active_highlight');
+
+    rangeEach(from, to, (column) => {
+      for (let level = this.columnHeaderLevelCount - 1; level > -1; level--) {
+        const visibleColumnIndex = this.getNestedParent(level, column);
+        const topTH = wtOverlays.topOverlay ? wtOverlays.topOverlay.clone.wtTable.getColumnHeader(visibleColumnIndex, level) : void 0;
+        const topLeftTH = wtOverlays.topLeftCornerOverlay ? wtOverlays.topLeftCornerOverlay.clone.wtTable.getColumnHeader(visibleColumnIndex, level) : void 0;
+        const listTH = [topTH, topLeftTH];
+        const colspanLen = this.getColspan(level - this.columnHeaderLevelCount, visibleColumnIndex);
+        const isInSelection = visibleColumnIndex >= from && (visibleColumnIndex + colspanLen - 1) <= to;
+
+        arrayEach(listTH, (TH) => {
+          if (TH === void 0) {
+            return false;
+          }
+
+          if ((!selectionByHeader && level < levelLimit) || (selectionByHeader && !isInSelection)) {
+            changes.push(highlightHeader(TH, removeClass));
+
+            if (selectionByHeader) {
+              changes.push(activeHeader(TH, removeClass));
+            }
+
+          } else {
+            changes.push(highlightHeader(TH, addClass));
+
+            if (selectionByHeader) {
+              changes.push(activeHeader(TH, addClass));
+            }
+          }
+        });
+      }
+    });
+
+    arrayEach(changes, fn => void fn());
+    changes.length = 0;
+  }
+
+  /**
+   * Make the renderer render the first nested column in its entirety.
+   *
+   * @private
+   * @param {Object} calc Viewport column calculator.
+   */
+  onAfterViewportColumnCalculatorOverride(calc) {
+    let newStartColumn = calc.startColumn;
+
+    rangeEach(0, Math.max(this.columnHeaderLevelCount - 1, 0), (l) => {
+      const startColumnNestedParent = this.getNestedParent(l, calc.startColumn);
+
+      if (startColumnNestedParent < calc.startColumn) {
+        newStartColumn = Math.min(newStartColumn, startColumnNestedParent);
+      }
+    });
+
+    calc.startColumn = newStartColumn;
+  }
+
+  /**
+   * Select all nested headers of clicked cell.
+   *
+   * @private
+   * @param {MouseEvent} event Mouse event.
+   * @param {Object} coords Clicked cell coords.
+   */
+  onAfterOnCellMouseDown(event, coords) {
+    if (coords.row < 0) {
+      const colspan = this.getColspan(coords.row, coords.col);
+      const lastColIndex = coords.col + colspan - 1;
+
+      if (colspan > 1) {
+        const lastRowIndex = this.hot.countRows() - 1;
+
+        this.hot.selection.setRangeEnd(new CellCoords(lastRowIndex, lastColIndex));
+      }
+    }
+  }
+
+  /**
+   * Make the header-selection properly select the nested headers.
+   *
+   * @private
+   * @param {MouseEvent} event Mouse event.
+   * @param {Object} coords Clicked cell coords.
+   * @param {HTMLElement} TD
+   */
+  onBeforeOnCellMouseOver(event, coords, TD, blockCalculations) {
+    if (coords.row >= 0 || coords.col < 0 || !this.hot.view.isMouseDown()) {
+      return;
+    }
+
+    const { from, to } = this.hot.getSelectedRangeLast();
+    const colspan = this.getColspan(coords.row, coords.col);
+    const lastColIndex = coords.col + colspan - 1;
+    let changeDirection = false;
+
+    if (from.col <= to.col) {
+      if ((coords.col < from.col && lastColIndex === to.col) ||
+          (coords.col < from.col && lastColIndex < from.col) ||
+          (coords.col < from.col && lastColIndex >= from.col && lastColIndex < to.col)) {
+        changeDirection = true;
+      }
+    } else if ((coords.col < to.col && lastColIndex > from.col) ||
+               (coords.col > from.col) ||
+               (coords.col <= to.col && lastColIndex > from.col) ||
+               (coords.col > to.col && lastColIndex > from.col)) {
+      changeDirection = true;
+    }
+
+    if (changeDirection) {
+      [from.col, to.col] = [to.col, from.col];
+    }
+
+    if (colspan > 1) {
+      blockCalculations.column = true;
+      blockCalculations.cell = true;
+
+      const columnRange = [];
+
+      if (from.col === to.col) {
+        if (lastColIndex <= from.col && coords.col < from.col) {
+          columnRange.push(to.col, coords.col);
+        } else {
+          columnRange.push(coords.col < from.col ? coords.col : from.col, lastColIndex > to.col ? lastColIndex : to.col);
+        }
+      }
+      if (from.col < to.col) {
+        columnRange.push(coords.col < from.col ? coords.col : from.col, lastColIndex);
+
+      }
+      if (from.col > to.col) {
+        columnRange.push(from.col, coords.col);
+      }
+
+      this.hot.selectColumns(...columnRange);
+    }
+  }
+
+  /**
+   * Cache column header count.
+   *
+   * @private
+   */
+  onAfterInit() {
+    this.columnHeaderLevelCount = this.hot.view.wt.getSetting('columnHeaders').length;
+
+    this.fillTheRemainingColspans();
+
+    this.checkForOverlappingHeaders();
+
+    this.ghostTable.buildWidthsMapper();
+  }
+
+  /**
+   * `afterGetColumnHeader` hook callback - prepares the header structure.
+   *
+   * @private
+   * @param {Array} renderersArray Array of renderers.
+   */
+  onAfterGetColumnHeaderRenderers(renderersArray) {
+    if (renderersArray) {
+      renderersArray.length = 0;
+
+      for (let headersCount = this.colspanArray.length, i = headersCount - 1; i >= 0; i--) {
+        renderersArray.push(this.headerRendererFactory(i));
+      }
+      renderersArray.reverse();
+    }
+
+    this.updateHeadersHighlight();
+  }
+
+  /**
+   * `modifyColWidth` hook callback - returns width from cache, when is greater than incoming from hook.
+   *
+   * @private
+   * @param width Width from hook.
+   * @param column Visual index of an column.
+   * @returns {Number}
+   */
+  onModifyColWidth(width, column) {
+    const cachedWidth = this.ghostTable.widthsCache[column];
+
+    return width > cachedWidth ? width : cachedWidth;
+  }
+
+  /**
+   * Destroys the plugin instance.
+   */
+  destroy() {
+    this.settings = null;
+    this.columnHeaderLevelCount = null;
+    this.colspanArray = null;
+
+    super.destroy();
+  }
+
+}
+
+registerPlugin('nestedHeaders', NestedHeaders);
+
+export default NestedHeaders;

+ 91 - 0
handsontable/src/plugins/nestedHeaders/test/ghostTable.e2e.js

@@ -0,0 +1,91 @@
+describe('NestedHeaders', () => {
+  const id = 'testContainer';
+
+  beforeEach(function() {
+    this.$container = $(`<div id="${id}"></div>`).appendTo('body');
+  });
+
+  afterEach(function() {
+    if (this.$container) {
+      destroy();
+      this.$container.remove();
+    }
+  });
+
+  describe('GhostTable', () => {
+    it('should be initialized and accessible from the plugin', () => {
+      const hot = handsontable({
+        data: Handsontable.helper.createSpreadsheetData(10, 10),
+        nestedHeaders: [
+          ['a', { label: 'b', colspan: 3 }, 'c', 'd'],
+          ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+        ]
+      });
+      const ghostTable = hot.getPlugin('nestedHeaders').ghostTable;
+
+      expect(ghostTable).toBeDefined();
+    });
+
+    describe('widthsCache', () => {
+      it('should contain cached widths after initialization', () => {
+        const hot = handsontable({
+          data: Handsontable.helper.createSpreadsheetData(10, 10),
+          nestedHeaders: [
+            ['a', { label: 'b', colspan: 3 }, 'c', 'd'],
+            ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+          ]
+        });
+        const ghostTable = hot.getPlugin('nestedHeaders').ghostTable;
+
+        expect(ghostTable.widthsCache.length).toBeGreaterThan(0);
+      });
+      it('should properly prepare widths cache, even if container is smaller than needed', () => {
+        const hot = handsontable({
+          data: Handsontable.helper.createSpreadsheetData(7, 7),
+          width: 300,
+          nestedHeaders: [
+            ['a', { label: 'b', colspan: 3 }, 'c', 'd', 'e'],
+            ['Very Long Title', 'Very Long Title', 'Very Long Title', 'Very Long Title', 'Very Long Title', 'Very Long Title', 'Very Long Title']
+          ]
+        });
+        const ghostTable = hot.getPlugin('nestedHeaders').ghostTable;
+
+        expect(ghostTable.widthsCache[ghostTable.widthsCache.length - 1]).toBeGreaterThan(50);
+      });
+      it('should container be removed after ', () => {
+        const hot = handsontable({
+          data: Handsontable.helper.createSpreadsheetData(10, 10),
+          nestedHeaders: [
+            ['a', { label: 'b', colspan: 3 }, 'c', 'd'],
+            ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+          ]
+        });
+        const ghostTable = hot.getPlugin('nestedHeaders').ghostTable;
+
+        expect(ghostTable.container).toBeNull();
+      });
+    });
+
+    describe('updateSettings', () => {
+      it('should recreate the widths cache', () => {
+        const hot = handsontable({
+          data: Handsontable.helper.createSpreadsheetData(10, 10),
+          nestedHeaders: [
+            ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+          ]
+        });
+        const beforeUpdate = hot.getPlugin('nestedHeaders').ghostTable.widthsCache[1];
+
+        hot.updateSettings({
+          nestedHeaders: [
+            ['a', 'bbbbbbbbbbbbbbbbb', 'c', 'd', 'e', 'f', 'g']
+          ]
+        });
+
+        const afterUpdate = hot.getPlugin('nestedHeaders').ghostTable.widthsCache[1];
+
+        expect(afterUpdate).toBeGreaterThan(beforeUpdate);
+      });
+    });
+  });
+});

+ 556 - 0
handsontable/src/plugins/nestedHeaders/test/nestedHeaders.e2e.js

@@ -0,0 +1,556 @@
+describe('NestedHeaders', () => {
+  const id = 'testContainer';
+
+  beforeEach(function() {
+    this.$container = $(`<div id="${id}"></div>`).appendTo('body');
+  });
+
+  afterEach(function() {
+    if (this.$container) {
+      destroy();
+      this.$container.remove();
+    }
+  });
+
+  describe('initialization', () => {
+    it('should be possible to disable the plugin using the disablePlugin method', () => {
+      const hot = handsontable({
+        data: Handsontable.helper.createSpreadsheetData(10, 10),
+        colHeaders: true,
+        nestedHeaders: [
+          ['a', { label: 'b', colspan: 3 }, 'c', 'd'],
+          ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+        ]
+      });
+
+      const plugin = hot.getPlugin('nestedHeaders');
+
+      expect($('TH[colspan=3]').size()).toBeGreaterThan(0);
+
+      plugin.disablePlugin();
+      hot.render();
+
+      expect($('TH[colspan=3]').size()).toEqual(0);
+    });
+
+    it('should be possible to re-enable the plugin using the enablePlugin method', () => {
+      const hot = handsontable({
+        data: Handsontable.helper.createSpreadsheetData(10, 10),
+        colHeaders: true,
+        nestedHeaders: [
+          ['a', { label: 'b', colspan: 3 }, 'c', 'd'],
+          ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+        ]
+      });
+
+      const plugin = hot.getPlugin('nestedHeaders');
+
+      plugin.disablePlugin();
+      hot.render();
+      plugin.enablePlugin();
+      hot.render();
+
+      expect($('TH[colspan=3]').size()).toBeGreaterThan(0);
+    });
+
+    it('should be possible to initialize the plugin using the updateSettings method', () => {
+      const hot = handsontable({
+        data: Handsontable.helper.createSpreadsheetData(10, 10),
+        colHeaders: true
+      });
+
+      expect($('TH[colspan=3]').size()).toEqual(0);
+
+      hot.updateSettings({
+        nestedHeaders: [
+          ['a', { label: 'b', colspan: 3 }, 'c', 'd'],
+          ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+        ]
+      });
+
+      expect($('TH[colspan=3]').size()).toBeGreaterThan(0);
+    });
+
+  });
+
+  describe('Basic functionality:', () => {
+    it('should add as many header levels as the \'colHeaders\' property suggests', () => {
+      const hot = handsontable({
+        data: Handsontable.helper.createSpreadsheetData(10, 10),
+        colHeaders: true,
+        nestedHeaders: [
+          ['a', 'b', 'c', 'd'],
+          ['a', 'b', 'c', 'd']
+        ]
+      });
+
+      expect(hot.view.wt.wtTable.THEAD.querySelectorAll('tr').length).toEqual(2);
+    });
+
+    it('should adjust headers widths', () => {
+      const hot = handsontable({
+        data: Handsontable.helper.createSpreadsheetData(10, 10),
+        colHeaders: true,
+        nestedHeaders: [
+          ['a', { label: 'b', colspan: 2 }, 'c', 'd'],
+          ['a', 'Long column header', 'c', 'd']
+        ]
+      });
+
+      const headers = hot.view.wt.wtTable.THEAD.querySelectorAll('tr:first-of-type th');
+
+      expect(hot.getColWidth(1)).toBeGreaterThan(50);
+      expect(headers[1].offsetWidth).toBeGreaterThan(100);
+    });
+  });
+
+  describe('The \'colspan\' property', () => {
+    it('should create nested headers, when using the \'colspan\' property', () => {
+      const hot = handsontable({
+        data: Handsontable.helper.createSpreadsheetData(10, 10),
+        colHeaders: true,
+        nestedHeaders: [
+          ['a', { label: 'b', colspan: 2 }, 'c', 'd'],
+          ['a', 'b', 'c', 'd', 'e']
+        ]
+      });
+
+      const headerRows = hot.view.wt.wtTable.THEAD.querySelectorAll('tr');
+
+      expect(headerRows[0].querySelector('th:nth-child(1)').getAttribute('colspan')).toEqual(null);
+      expect(headerRows[0].querySelector('th:nth-child(2)').getAttribute('colspan')).toEqual('2');
+      expect(headerRows[0].querySelector('th:nth-child(3)').getAttribute('colspan')).toEqual(null);
+
+      expect(headerRows[1].querySelector('th:nth-child(1)').getAttribute('colspan')).toEqual(null);
+      expect(headerRows[1].querySelector('th:nth-child(2)').getAttribute('colspan')).toEqual(null);
+      expect(headerRows[1].querySelector('th:nth-child(3)').getAttribute('colspan')).toEqual(null);
+
+    });
+
+    it('should allow creating a more complex nested setup', () => {
+      const hot = handsontable({
+        data: Handsontable.helper.createSpreadsheetData(10, 10),
+        colHeaders: true,
+        nestedHeaders: [
+          ['a', { label: 'b', colspan: 4 }, 'c', 'd'],
+          ['a', { label: 'b', colspan: 2 }, { label: 'c', colspan: 2 }, 'd', 'e']
+        ]
+      });
+
+      const headerRows = hot.view.wt.wtTable.THEAD.querySelectorAll('tr');
+      const nonHiddenTHs = function(row) {
+        return headerRows[row].querySelectorAll('th:not(.hiddenHeader)');
+      };
+      const firstLevel = nonHiddenTHs(0);
+      const secondLevel = nonHiddenTHs(1);
+
+      expect(firstLevel[0].getAttribute('colspan')).toEqual(null);
+      expect(firstLevel[1].getAttribute('colspan')).toEqual('4');
+      expect(firstLevel[2].getAttribute('colspan')).toEqual(null);
+
+      expect(secondLevel[0].getAttribute('colspan')).toEqual(null);
+      expect(secondLevel[1].getAttribute('colspan')).toEqual('2');
+      expect(secondLevel[2].getAttribute('colspan')).toEqual('2');
+      expect(secondLevel[3].getAttribute('colspan')).toEqual(null);
+    });
+
+    it('should render the setup properly after the table being scrolled', () => {
+      function generateComplexSetup(rows, cols, obj) {
+        const data = [];
+
+        for (let i = 0; i < rows; i++) {
+          for (let j = 0; j < cols; j++) {
+            if (!data[i]) {
+              data[i] = [];
+            }
+
+            if (!obj) {
+              data[i][j] = `${i}_${j}`;
+              /* eslint-disable no-continue */
+              continue;
+            }
+
+            if (i === 0 && j % 2 !== 0) {
+              data[i][j] = {
+                label: `${i}_${j}`,
+                colspan: 8
+              };
+            } else if (i === 1 && (j % 3 === 1 || j % 3 === 2)) {
+              data[i][j] = {
+                label: `${i}_${j}`,
+                colspan: 4
+              };
+            } else if (i === 2 && (j % 5 === 1 || j % 5 === 2 || j % 5 === 3 || j % 5 === 4)) {
+              data[i][j] = {
+                label: `${i}_${j}`,
+                colspan: 2
+              };
+            } else {
+              data[i][j] = `${i}_${j}`;
+            }
+
+          }
+        }
+
+        return data;
+      }
+
+      const hot = handsontable({
+        data: Handsontable.helper.createSpreadsheetData(10, 90),
+        colHeaders: true,
+        nestedHeaders: generateComplexSetup(4, 70, true),
+        width: 400,
+        height: 300,
+        viewportColumnRenderingOffset: 15
+      });
+
+      const headerRows = hot.view.wt.wtTable.THEAD.querySelectorAll('tr');
+      const nonHiddenTHs = function(row) {
+        return headerRows[row].querySelectorAll('th:not(.hiddenHeader)');
+      };
+      let levels = [nonHiddenTHs(0), nonHiddenTHs(1), nonHiddenTHs(2), nonHiddenTHs(3)];
+
+      // not scrolled
+      expect(levels[0][0].getAttribute('colspan')).toEqual(null);
+      expect(levels[0][1].getAttribute('colspan')).toEqual('8');
+      expect(levels[0][2].getAttribute('colspan')).toEqual(null);
+      expect(levels[0][3].getAttribute('colspan')).toEqual('8');
+
+      expect(levels[1][0].getAttribute('colspan')).toEqual(null);
+      expect(levels[1][1].getAttribute('colspan')).toEqual('4');
+      expect(levels[1][2].getAttribute('colspan')).toEqual('4');
+      expect(levels[1][3].getAttribute('colspan')).toEqual(null);
+
+      expect(levels[2][0].getAttribute('colspan')).toEqual(null);
+      expect(levels[2][1].getAttribute('colspan')).toEqual('2');
+      expect(levels[2][2].getAttribute('colspan')).toEqual('2');
+      expect(levels[2][3].getAttribute('colspan')).toEqual('2');
+
+      expect(levels[3][0].getAttribute('colspan')).toEqual(null);
+      expect(levels[3][1].getAttribute('colspan')).toEqual(null);
+      expect(levels[3][2].getAttribute('colspan')).toEqual(null);
+      expect(levels[3][3].getAttribute('colspan')).toEqual(null);
+
+      hot.scrollViewportTo(void 0, 40);
+      hot.render();
+
+      levels = [nonHiddenTHs(0), nonHiddenTHs(1), nonHiddenTHs(2), nonHiddenTHs(3)];
+
+      // scrolled
+      expect(levels[0][0].getAttribute('colspan')).toEqual('8');
+      expect(levels[0][1].getAttribute('colspan')).toEqual(null);
+      expect(levels[0][2].getAttribute('colspan')).toEqual('8');
+      expect(levels[0][3].getAttribute('colspan')).toEqual(null);
+
+      expect(levels[1][0].getAttribute('colspan')).toEqual('4');
+      expect(levels[1][1].getAttribute('colspan')).toEqual('4');
+      expect(levels[1][2].getAttribute('colspan')).toEqual(null);
+      expect(levels[1][3].getAttribute('colspan')).toEqual('4');
+
+      expect(levels[2][0].getAttribute('colspan')).toEqual('2');
+      expect(levels[2][1].getAttribute('colspan')).toEqual('2');
+      expect(levels[2][2].getAttribute('colspan')).toEqual('2');
+      expect(levels[2][3].getAttribute('colspan')).toEqual('2');
+
+      expect(levels[3][0].getAttribute('colspan')).toEqual(null);
+      expect(levels[3][1].getAttribute('colspan')).toEqual(null);
+      expect(levels[3][2].getAttribute('colspan')).toEqual(null);
+      expect(levels[3][3].getAttribute('colspan')).toEqual(null);
+    });
+
+  });
+
+  describe('Selection:', () => {
+    it('should select every column under the extended header', function() {
+      const hot = handsontable({
+        data: Handsontable.helper.createSpreadsheetData(10, 10),
+        colHeaders: true,
+        nestedHeaders: [
+          ['A', { label: 'B', colspan: 8 }, 'C'],
+          ['D', { label: 'E', colspan: 4 }, { label: 'F', colspan: 4 }, 'G'],
+          ['H', { label: 'I', colspan: 2 }, { label: 'J', colspan: 2 }, { label: 'K', colspan: 2 }, { label: 'L', colspan: 2 }, 'M'],
+          ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W']
+        ]
+      });
+
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(1)').simulate('mousedown');
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(1)').simulate('mouseup');
+
+      expect(this.$container.find('.ht_clone_top thead tr:eq(0) th').map((_, el) => $(el).attr('class')).toArray()).toEqual([
+        '',
+        '',
+        'hiddenHeader ht__highlight ht__active_highlight',
+        'hiddenHeader',
+        'hiddenHeader',
+        'hiddenHeader',
+        'hiddenHeader',
+        'hiddenHeader',
+        'hiddenHeader',
+        '',
+      ]);
+      expect(this.$container.find('.ht_clone_top thead tr:eq(1) th').map((_, el) => $(el).attr('class')).toArray()).toEqual([
+        '',
+        '',
+        'hiddenHeader',
+        'hiddenHeader',
+        'hiddenHeader',
+        '',
+        'hiddenHeader',
+        'hiddenHeader',
+        'hiddenHeader',
+        '',
+      ]);
+      expect(this.$container.find('.ht_clone_top thead tr:eq(2) th').map((_, el) => $(el).attr('class')).toArray()).toEqual([
+        '',
+        'ht__highlight ht__active_highlight',
+        'hiddenHeader',
+        '',
+        'hiddenHeader',
+        '',
+        'hiddenHeader',
+        '',
+        'hiddenHeader',
+        '',
+      ]);
+      expect(this.$container.find('.ht_clone_top thead tr:eq(3) th').map((_, el) => $(el).attr('class')).toArray()).toEqual([
+        '',
+        'ht__highlight ht__active_highlight',
+        'ht__highlight ht__active_highlight',
+        '',
+        '',
+        '',
+        '',
+        '',
+        '',
+        '',
+      ]);
+
+      expect(hot.getSelected()).toEqual([[0, 1, hot.countRows() - 1, 2]]);
+
+      this.$container.find('.ht_clone_top thead tr:eq(1) th:eq(1)').simulate('mousedown');
+      this.$container.find('.ht_clone_top thead tr:eq(1) th:eq(1)').simulate('mouseup');
+
+      expect(this.$container.find('.ht_clone_top thead tr:eq(0) th').map((_, el) => $(el).attr('class')).toArray()).toEqual([
+        '',
+        '',
+        'hiddenHeader ht__highlight ht__active_highlight',
+        'hiddenHeader ht__highlight ht__active_highlight',
+        'hiddenHeader ht__highlight ht__active_highlight',
+        'hiddenHeader',
+        'hiddenHeader',
+        'hiddenHeader',
+        'hiddenHeader',
+        '',
+      ]);
+      expect(this.$container.find('.ht_clone_top thead tr:eq(1) th').map((_, el) => $(el).attr('class')).toArray()).toEqual([
+        '',
+        'ht__highlight ht__active_highlight',
+        'hiddenHeader',
+        'hiddenHeader',
+        'hiddenHeader',
+        '',
+        'hiddenHeader',
+        'hiddenHeader',
+        'hiddenHeader',
+        '',
+      ]);
+      expect(this.$container.find('.ht_clone_top thead tr:eq(2) th').map((_, el) => $(el).attr('class')).toArray()).toEqual([
+        '',
+        'ht__highlight ht__active_highlight',
+        'hiddenHeader',
+        'ht__highlight ht__active_highlight',
+        'hiddenHeader',
+        '',
+        'hiddenHeader',
+        '',
+        'hiddenHeader',
+        '',
+      ]);
+      expect(this.$container.find('.ht_clone_top thead tr:eq(3) th').map((_, el) => $(el).attr('class')).toArray()).toEqual([
+        '',
+        'ht__highlight ht__active_highlight',
+        'ht__highlight ht__active_highlight',
+        'ht__highlight ht__active_highlight',
+        'ht__highlight ht__active_highlight',
+        '',
+        '',
+        '',
+        '',
+        '',
+      ]);
+
+      expect(hot.getSelected()).toEqual([[0, 1, hot.countRows() - 1, 4]]);
+
+      this.$container.find('.ht_clone_top thead tr:eq(0) th:eq(1)').simulate('mousedown');
+      this.$container.find('.ht_clone_top thead tr:eq(0) th:eq(1)').simulate('mouseup');
+
+      expect(this.$container.find('.ht_clone_top thead tr:eq(0) th').map((_, el) => $(el).attr('class')).toArray()).toEqual([
+        '',
+        'ht__highlight ht__active_highlight',
+        'hiddenHeader ht__highlight ht__active_highlight',
+        'hiddenHeader ht__highlight ht__active_highlight',
+        'hiddenHeader ht__highlight ht__active_highlight',
+        'hiddenHeader ht__highlight ht__active_highlight',
+        'hiddenHeader ht__highlight ht__active_highlight',
+        'hiddenHeader ht__highlight ht__active_highlight',
+        'hiddenHeader ht__highlight ht__active_highlight',
+        '',
+      ]);
+      expect(this.$container.find('.ht_clone_top thead tr:eq(1) th').map((_, el) => $(el).attr('class')).toArray()).toEqual([
+        '',
+        'ht__highlight ht__active_highlight',
+        'hiddenHeader',
+        'hiddenHeader',
+        'hiddenHeader',
+        'ht__highlight ht__active_highlight',
+        'hiddenHeader',
+        'hiddenHeader',
+        'hiddenHeader',
+        '',
+      ]);
+      expect(this.$container.find('.ht_clone_top thead tr:eq(2) th').map((_, el) => $(el).attr('class')).toArray()).toEqual([
+        '',
+        'ht__highlight ht__active_highlight',
+        'hiddenHeader',
+        'ht__highlight ht__active_highlight',
+        'hiddenHeader',
+        'ht__highlight ht__active_highlight',
+        'hiddenHeader',
+        'ht__highlight ht__active_highlight',
+        'hiddenHeader',
+        '',
+      ]);
+      expect(this.$container.find('.ht_clone_top thead tr:eq(3) th').map((_, el) => $(el).attr('class')).toArray()).toEqual([
+        '',
+        'ht__highlight ht__active_highlight',
+        'ht__highlight ht__active_highlight',
+        'ht__highlight ht__active_highlight',
+        'ht__highlight ht__active_highlight',
+        'ht__highlight ht__active_highlight',
+        'ht__highlight ht__active_highlight',
+        'ht__highlight ht__active_highlight',
+        'ht__highlight ht__active_highlight',
+        '',
+      ]);
+
+      expect(hot.getSelected()).toEqual([[0, 1, hot.countRows() - 1, 8]]);
+    });
+
+    it('should select every column under the extended headers, when changing the selection by dragging the cursor', function() {
+      const hot = handsontable({
+        data: Handsontable.helper.createSpreadsheetData(10, 10),
+        colHeaders: true,
+        nestedHeaders: [
+          ['A', { label: 'B', colspan: 8 }, 'C'],
+          ['D', { label: 'E', colspan: 4 }, { label: 'F', colspan: 4 }, 'G'],
+          ['H', { label: 'I', colspan: 2 }, { label: 'J', colspan: 2 }, { label: 'K', colspan: 2 }, { label: 'L', colspan: 2 }, 'M'],
+          ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W']
+        ]
+      });
+
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(3)').simulate('mousedown');
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(5)').simulate('mouseover');
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(5)').simulate('mouseup');
+
+      expect(hot.getSelected()).toEqual([[0, 3, hot.countRows() - 1, 6]]);
+
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(3)').simulate('mousedown');
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(1)').simulate('mouseover');
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(1)').simulate('mouseup');
+
+      expect(hot.getSelected()).toEqual([[0, 4, hot.countRows() - 1, 1]]);
+
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(3)').simulate('mousedown');
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(1)').simulate('mouseover');
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(3)').simulate('mouseover');
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(5)').simulate('mouseover');
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(5)').simulate('mouseup');
+
+      expect(hot.getSelected()).toEqual([[0, 3, hot.countRows() - 1, 6]]);
+    });
+
+    it('should highlight only last line of headers on cell selection', function() {
+      handsontable({
+        data: Handsontable.helper.createSpreadsheetData(10, 10),
+        colHeaders: true,
+        nestedHeaders: [
+          ['A', { label: 'B', colspan: 8 }, 'C'],
+          ['D', { label: 'E', colspan: 4 }, { label: 'F', colspan: 4 }, 'G'],
+          ['H', { label: 'I', colspan: 2 }, { label: 'J', colspan: 2 }, { label: 'K', colspan: 2 }, { label: 'L', colspan: 2 }, 'M'],
+          ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W']
+        ]
+      });
+
+      this.$container.find('.ht_master tbody tr:eq(2) td:eq(1)').simulate('mousedown');
+      this.$container.find('.ht_master tbody tr:eq(2) td:eq(1)').simulate('mouseup');
+
+      const headerLvl3 = this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(1)');
+      const headerLvl4 = this.$container.find('.ht_clone_top thead tr:eq(3) th:eq(1)');
+
+      expect(headerLvl3.hasClass('ht__highlight')).toBeFalsy();
+      expect(headerLvl4.hasClass('ht__highlight')).toBeTruthy();
+    });
+
+    it('should highlight every header which was in selection on headers selection', function() {
+      handsontable({
+        data: Handsontable.helper.createSpreadsheetData(10, 10),
+        colHeaders: true,
+        nestedHeaders: [
+          ['A', { label: 'B', colspan: 8 }, 'C'],
+          ['D', { label: 'E', colspan: 4 }, { label: 'F', colspan: 4 }, 'G'],
+          ['H', { label: 'I', colspan: 2 }, { label: 'J', colspan: 2 }, { label: 'K', colspan: 2 }, { label: 'L', colspan: 2 }, 'M'],
+          ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W']
+        ]
+      });
+
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(1)').simulate('mousedown');
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(1)').simulate('mouseup');
+
+      const headerLvl2 = this.$container.find('.ht_clone_top thead tr:eq(1) th:eq(1)');
+      const headerLvl3 = this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(1)');
+      const headerLvl41 = this.$container.find('.ht_clone_top thead tr:eq(3) th:eq(1)');
+      const headerLvl42 = this.$container.find('.ht_clone_top thead tr:eq(3) th:eq(2)');
+
+      expect(headerLvl2.hasClass('ht__highlight')).toBeFalsy();
+      expect(headerLvl3.hasClass('ht__highlight')).toBeTruthy();
+      expect(headerLvl41.hasClass('ht__highlight')).toBeTruthy();
+      expect(headerLvl42.hasClass('ht__highlight')).toBeTruthy();
+    });
+
+    it('should add selection borders in the expected positions, when selecting multi-columned headers', function() {
+      handsontable({
+        data: Handsontable.helper.createSpreadsheetData(4, 10),
+        colHeaders: true,
+        nestedHeaders: [
+          ['A', { label: 'B', colspan: 8 }, 'C'],
+          ['D', { label: 'E', colspan: 4 }, { label: 'F', colspan: 4 }, 'G'],
+          ['H', { label: 'I', colspan: 2 }, { label: 'J', colspan: 2 }, { label: 'K', colspan: 2 }, { label: 'L', colspan: 2 }, 'M'],
+          ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W']
+        ]
+      });
+
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(1)').simulate('mousedown');
+      this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(1)').simulate('mouseup');
+
+      const $headerLvl3 = this.$container.find('.ht_clone_top thead tr:eq(2) th:eq(1)');
+      const $firstRow = this.$container.find('.ht_master tbody tr:eq(0)');
+      const $lastRow = this.$container.find('.ht_master tbody tr:eq(3)');
+      const $tbody = this.$container.find('.ht_master tbody');
+
+      const $topBorder = this.$container.find('.wtBorder.area').eq(0);
+      const $bottomBorder = this.$container.find('.wtBorder.area').eq(2);
+      const $leftBorder = this.$container.find('.wtBorder.area').eq(1);
+      const $rightBorder = this.$container.find('.wtBorder.area').eq(3);
+
+      expect($topBorder.offset().top).toEqual($firstRow.offset().top - 1);
+      expect($bottomBorder.offset().top).toEqual($lastRow.offset().top + $lastRow.height() - 1);
+      expect($topBorder.width()).toEqual($headerLvl3.width());
+      expect($bottomBorder.width()).toEqual($headerLvl3.width());
+
+      expect($leftBorder.offset().left).toEqual($headerLvl3.offset().left);
+      expect($rightBorder.offset().left).toEqual($headerLvl3.offset().left + $headerLvl3.width());
+      expect($leftBorder.height()).toEqual($tbody.height());
+      expect($rightBorder.height()).toEqual($tbody.height() + 1);
+    });
+  });
+});

+ 122 - 0
handsontable/src/plugins/nestedHeaders/utils/ghostTable.js

@@ -0,0 +1,122 @@
+import { fastInnerHTML } from '../../../helpers/dom/element';
+import { clone } from '../../../helpers/object';
+
+class GhostTable {
+  constructor(plugin) {
+    /**
+     * Reference to NestedHeaders plugin.
+     *
+     * @type {NestedHeaders}
+     */
+    this.nestedHeaders = plugin;
+    /**
+     * Temporary element created to get minimal headers widths.
+     *
+     * @type {*}
+     */
+    this.container = void 0;
+    /**
+     * Cached the headers widths.
+     *
+     * @type {Array}
+     */
+    this.widthsCache = [];
+  }
+
+  /**
+   * Build cache of the headers widths.
+   *
+   * @private
+   */
+  buildWidthsMapper() {
+    this.container = document.createElement('div');
+
+    this.buildGhostTable(this.container);
+    this.nestedHeaders.hot.rootElement.appendChild(this.container);
+
+    const columns = this.container.querySelectorAll('tr:last-of-type th');
+    const maxColumns = columns.length;
+
+    for (let i = 0; i < maxColumns; i++) {
+      this.widthsCache.push(columns[i].offsetWidth);
+    }
+
+    this.container.parentNode.removeChild(this.container);
+    this.container = null;
+
+    this.nestedHeaders.hot.render();
+  }
+
+  /**
+   * Build temporary table for getting minimal columns widths.
+   *
+   * @private
+   * @param {HTMLElement} container
+   */
+  buildGhostTable(container) {
+    const d = document;
+    const fragment = d.createDocumentFragment();
+    const table = d.createElement('table');
+    let lastRowColspan = false;
+    const isDropdownEnabled = !!this.nestedHeaders.hot.getSettings().dropdownMenu;
+    const maxRows = this.nestedHeaders.colspanArray.length;
+    const maxCols = this.nestedHeaders.hot.countCols();
+    const lastRowIndex = maxRows - 1;
+
+    for (let row = 0; row < maxRows; row++) {
+      const tr = d.createElement('tr');
+
+      lastRowColspan = false;
+
+      for (let col = 0; col < maxCols; col++) {
+        const td = d.createElement('th');
+        const headerObj = clone(this.nestedHeaders.colspanArray[row][col]);
+
+        if (headerObj && !headerObj.hidden) {
+          if (row === lastRowIndex) {
+            if (headerObj.colspan > 1) {
+              lastRowColspan = true;
+            }
+            if (isDropdownEnabled) {
+              headerObj.label += '<button class="changeType"></button>';
+            }
+          }
+
+          fastInnerHTML(td, headerObj.label);
+          td.colSpan = headerObj.colspan;
+          tr.appendChild(td);
+        }
+      }
+
+      table.appendChild(tr);
+    }
+
+    // We have to be sure the last row contains only the single columns.
+    if (lastRowColspan) {
+      {
+        const tr = d.createElement('tr');
+
+        for (let col = 0; col < maxCols; col++) {
+          const td = d.createElement('th');
+          tr.appendChild(td);
+        }
+
+        table.appendChild(tr);
+      }
+    }
+
+    fragment.appendChild(table);
+    container.appendChild(fragment);
+  }
+
+  /**
+   * Clear the widths cache.
+   */
+  clear() {
+    this.container = null;
+    this.widthsCache.length = 0;
+  }
+
+}
+
+export default GhostTable;

+ 0 - 191
handsontable/src/plugins/shieldRows/shieldRows.js

@@ -1,191 +0,0 @@
-import BasePlugin from '../_base';
-import { arrayEach } from '../../helpers/array';
-import { rangeEach } from '../../helpers/number';
-import { registerPlugin } from '../../plugins';
-import { mixin } from '../../helpers/object';
-import arrayMapper from '../../mixins/arrayMapper';
-
-class Mapper {
-  constructor(shieldRows) {
-    this.shieldRows = shieldRows;
-  }
-
-  createMap(length) {
-    let rowOffset = 0;
-    const originLength = length ? this._arrayMap.length : length;
-
-    this._arrayMap.length = 0;
-
-    rangeEach(originLength - 1, (itemIndex) => {
-      if (this.shieldRows.isShield(itemIndex)) {
-        rowOffset += 1;
-      } else {
-        this._arrayMap[itemIndex - rowOffset] = itemIndex;
-      }
-    });
-  }
-
-  destroy() {
-    this._arrayMap = null;
-  }
-}
-
-mixin(Mapper, arrayMapper);
-
-class TrimRows extends BasePlugin {
-  constructor(hotInstance) {
-    super(hotInstance);
-
-    this.trimmedRows = [];
-
-    this.removedRows = [];
-
-    this.rowsMapper = new Mapper(this);
-  }
-
-  isEnabled() {
-    return !!this.hot.getSettings().trimRows;
-  }
-
-  enablePlugin() {
-    if (this.enabled) {
-      return;
-    }
-    const settings = this.hot.getSettings().trimRows;
-
-    if (Array.isArray(settings)) {
-      this.trimmedRows = settings;
-    }
-    this.rowsMapper.createMap(this.hot.countSourceRows());
-
-    this.addHook('modifyRow', (row, source) => this.onModifyRow(row, source));
-    this.addHook('unmodifyRow', (row, source) => this.onUnmodifyRow(row, source));
-    this.addHook('beforeCreateRow', (index, amount, source) => this.onBeforeCreateRow(index, amount, source));
-    this.addHook('afterCreateRow', (index, amount) => this.onAfterCreateRow(index, amount));
-    this.addHook('beforeRemoveRow', (index, amount) => this.onBeforeRemoveRow(index, amount));
-    this.addHook('afterRemoveRow', () => this.onAfterRemoveRow());
-    this.addHook('afterLoadData', firstRun => this.onAfterLoadData(firstRun));
-
-    super.enablePlugin();
-  }
-
-  updatePlugin() {
-    const settings = this.hot.getSettings().trimRows;
-
-    if (Array.isArray(settings)) {
-      this.disablePlugin();
-      this.enablePlugin();
-    }
-
-    super.updatePlugin();
-  }
-
-  disablePlugin() {
-    this.trimmedRows = [];
-    this.removedRows.length = 0;
-    this.rowsMapper.clearMap();
-    super.disablePlugin();
-  }
-
-  trimRows(rows) {
-    arrayEach(rows, (row) => {
-      const physicalRow = parseInt(row, 10);
-
-      if (!this.isShield(physicalRow)) {
-        this.trimmedRows.push(physicalRow);
-      }
-    });
-
-    this.hot.runHooks('skipLengthCache', 100);
-    this.rowsMapper.createMap(this.hot.countSourceRows());
-    this.hot.runHooks('afterTrimRow', rows);
-  }
-
-  trimRow(...row) {
-    this.trimRows(row);
-  }
-
-  untrimRows(rows) {
-    arrayEach(rows, (row) => {
-      const physicalRow = parseInt(row, 10);
-
-      if (this.isShield(physicalRow)) {
-        this.trimmedRows.splice(this.trimmedRows.indexOf(physicalRow), 1);
-      }
-    });
-
-    this.hot.runHooks('skipLengthCache', 100);
-    this.rowsMapper.createMap(this.hot.countSourceRows());
-    this.hot.runHooks('afterUntrimRow', rows);
-  }
-
-  untrimRow(...row) {
-    this.untrimRows(row);
-  }
-
-  isShield(row) {
-    return this.trimmedRows.indexOf(row) > -1;
-  }
-
-  untrimAll() {
-    this.untrimRows([].concat(this.trimmedRows));
-  }
-
-  onModifyRow(row, source) {
-    let physicalRow = row;
-
-    if (source !== this.pluginName) {
-      physicalRow = this.rowsMapper.getValueByIndex(physicalRow);
-    }
-
-    return physicalRow;
-  }
-
-  onUnmodifyRow(row, source) {
-    let visualRow = row;
-
-    if (source !== this.pluginName) {
-      visualRow = this.rowsMapper.getIndexByValue(visualRow);
-    }
-
-    return visualRow;
-  }
-
-  onBeforeCreateRow(index, amount, source) {
-    return !(this.isEnabled() && this.trimmedRows.length > 0 && source === 'auto');
-  }
-
-  onAfterCreateRow(index, amount) {
-    this.rowsMapper.shiftItems(index, amount);
-  }
-
-  onBeforeRemoveRow(index, amount) {
-    this.removedRows.length = 0;
-
-    if (index !== false) {
-      // Collect physical row index.
-      rangeEach(index, index + amount - 1, (removedIndex) => {
-        this.removedRows.push(this.hot.runHooks('modifyRow', removedIndex, this.pluginName));
-      });
-    }
-  }
-
-  onAfterRemoveRow() {
-    this.rowsMapper.unshiftItems(this.removedRows);
-  }
-
-  onAfterLoadData(firstRun) {
-    if (!firstRun) {
-      this.rowsMapper.createMap(this.hot.countSourceRows());
-    }
-  }
-
-  destroy() {
-    this.rowsMapper.destroy();
-    super.destroy();
-  }
-}
-
-registerPlugin('trimRows', TrimRows);
-
-export default TrimRows;

+ 0 - 50
handsontable/src/plugins/trimRows/rowsMapper.js

@@ -1,50 +0,0 @@
-import arrayMapper from '../../mixins/arrayMapper';
-import { mixin } from '../../helpers/object';
-import { rangeEach } from '../../helpers/number';
-
-/**
- * @class RowsMapper
- * @plugin TrimRows
- * @pro
- */
-class RowsMapper {
-  constructor(trimRows) {
-    /**
-     * Instance of TrimRows plugin.
-     *
-     * @type {TrimRows}
-     */
-    this.trimRows = trimRows;
-  }
-
-  /**
-   * Reset current map array and create new one.
-   *
-   * @param {Number} [length] Custom generated map length.
-   */
-  createMap(length) {
-    let rowOffset = 0;
-    const originLength = length === void 0 ? this._arrayMap.length : length;
-
-    this._arrayMap.length = 0;
-
-    rangeEach(originLength - 1, (itemIndex) => {
-      if (this.trimRows.isTrimmed(itemIndex)) {
-        rowOffset += 1;
-      } else {
-        this._arrayMap[itemIndex - rowOffset] = itemIndex;
-      }
-    });
-  }
-
-  /**
-   * Destroy class.
-   */
-  destroy() {
-    this._arrayMap = null;
-  }
-}
-
-mixin(RowsMapper, arrayMapper);
-
-export default RowsMapper;

+ 0 - 81
handsontable/src/plugins/trimRows/test/rowsMapper.unit.js

@@ -1,81 +0,0 @@
-import RowsMapper from '../../trimRows/rowsMapper';
-
-describe('TrimRows -> RowsMapper', () => {
-  it('should set trimRows plugin while constructing', () => {
-    const trimRowsMock = {};
-    const mapper = new RowsMapper(trimRowsMock);
-
-    expect(mapper.trimRows).toBe(trimRowsMock);
-  });
-
-  it('should be mixed with arrayMapper object', () => {
-    expect(RowsMapper.MIXINS).toEqual(['arrayMapper']);
-  });
-
-  it('should destroy array after calling destroy method', () => {
-    const mapper = new RowsMapper();
-
-    expect(mapper._arrayMap).toEqual([]);
-
-    mapper.destroy();
-
-    expect(mapper._arrayMap).toBe(null);
-  });
-
-  it('should call isTrimmed method "length" times', () => {
-    const trimRowsMock = {
-      isTrimmed() {
-        return false;
-      }
-    };
-    const mapper = new RowsMapper(trimRowsMock);
-
-    spyOn(trimRowsMock, 'isTrimmed').and.callThrough();
-    mapper.createMap(5);
-
-    expect(trimRowsMock.isTrimmed.calls.count()).toBe(5);
-    expect(trimRowsMock.isTrimmed.calls.argsFor(0)).toEqual([0]);
-    expect(trimRowsMock.isTrimmed.calls.argsFor(1)).toEqual([1]);
-    expect(trimRowsMock.isTrimmed.calls.argsFor(2)).toEqual([2]);
-    expect(trimRowsMock.isTrimmed.calls.argsFor(3)).toEqual([3]);
-    expect(trimRowsMock.isTrimmed.calls.argsFor(4)).toEqual([4]);
-  });
-
-  it('should create map with pairs index->value', () => {
-    const trimRowsMock = {
-      isTrimmed() {
-        return false;
-      }
-    };
-    const mapper = new RowsMapper(trimRowsMock);
-
-    spyOn(trimRowsMock, 'isTrimmed').and.callThrough();
-    mapper.createMap(6);
-
-    expect(mapper._arrayMap[0]).toBe(0);
-    expect(mapper._arrayMap[1]).toBe(1);
-    expect(mapper._arrayMap[2]).toBe(2);
-    expect(mapper._arrayMap[3]).toBe(3);
-    expect(mapper._arrayMap[4]).toBe(4);
-    expect(mapper._arrayMap[5]).toBe(5);
-  });
-
-  it('should create map with pairs index->value with some gaps', () => {
-    const trimRowsMock = {
-      isTrimmed(index) {
-        return index === 2 || index === 5;
-      }
-    };
-    const mapper = new RowsMapper(trimRowsMock);
-
-    spyOn(trimRowsMock, 'isTrimmed').and.callThrough();
-    mapper.createMap(6);
-
-    expect(mapper._arrayMap[0]).toBe(0);
-    expect(mapper._arrayMap[1]).toBe(1);
-    expect(mapper._arrayMap[2]).toBe(3);
-    expect(mapper._arrayMap[3]).toBe(4);
-    expect(mapper._arrayMap[4]).toBeUndefined();
-    expect(mapper._arrayMap[5]).toBeUndefined();
-  });
-});

+ 0 - 668
handsontable/src/plugins/trimRows/test/trimRows.e2e.js

@@ -1,668 +0,0 @@
-describe('TrimRows', () => {
-  const id = 'testContainer';
-
-  function getMultilineData(rows, cols) {
-    const data = Handsontable.helper.createSpreadsheetData(rows, cols);
-
-    // Column C
-    data[0][2] += '\nline';
-    data[1][2] += '\nline\nline';
-
-    return data;
-  }
-
-  beforeEach(function() {
-    this.$container = $(`<div id="${id}"></div>`).appendTo('body');
-  });
-
-  afterEach(function() {
-    if (this.$container) {
-      destroy();
-      this.$container.remove();
-    }
-  });
-
-  it('should trim rows defined in `trimRows` property', () => {
-    handsontable({
-      data: Handsontable.helper.createSpreadsheetData(10, 10),
-      trimRows: [2, 6, 7],
-      cells(row) {
-        const meta = {};
-
-        if (row === 2) {
-          meta.type = 'date';
-        }
-
-        return meta;
-      },
-      width: 500,
-      height: 300
-    });
-
-    expect(getDataAtCell(0, 0)).toBe('A1');
-    expect(getDataAtCell(1, 0)).toBe('A2');
-    expect(getDataAtCell(2, 0)).toBe('A4');
-    expect(getDataAtCell(3, 0)).toBe('A5');
-    expect(getDataAtCell(4, 0)).toBe('A6');
-    expect(getDataAtCell(5, 0)).toBe('A9');
-    expect(getDataAtCell(6, 0)).toBe('A10');
-    expect(getCellMeta(0, 0).type).toBe('text');
-    expect(getCellMeta(1, 0).type).toBe('text');
-    expect(getCellMeta(2, 0).type).toBe('text');
-    expect(getCellMeta(3, 0).type).toBe('text');
-    expect(getCellMeta(4, 0).type).toBe('text');
-    expect(getCellMeta(5, 0).type).toBe('text');
-    expect(getCellMeta(6, 0).type).toBe('text');
-  });
-
-  it('should trim rows after re-load data calling loadData method', () => {
-    const hot = handsontable({
-      data: Handsontable.helper.createSpreadsheetData(10, 10),
-      trimRows: [0, 2],
-      width: 500,
-      height: 300
-    });
-
-    hot.loadData(Handsontable.helper.createSpreadsheetData(5, 5));
-
-    expect(getDataAtCell(0, 0)).toBe('A2');
-    expect(getDataAtCell(1, 0)).toBe('A4');
-    expect(getDataAtCell(2, 0)).toBe('A5');
-    expect(getDataAtCell(3, 0)).toBe(null);
-    expect(getDataAtCell(4, 0)).toBe(null);
-  });
-
-  it('should return to default state after call disablePlugin method', () => {
-    const hot = handsontable({
-      data: getMultilineData(10, 10),
-      trimRows: [2, 6, 7],
-      width: 500,
-      height: 300
-    });
-    hot.getPlugin('trimRows').disablePlugin();
-    hot.render();
-
-    expect(getDataAtCell(0, 0)).toBe('A1');
-    expect(getDataAtCell(1, 0)).toBe('A2');
-    expect(getDataAtCell(2, 0)).toBe('A3');
-    expect(getDataAtCell(3, 0)).toBe('A4');
-    expect(getDataAtCell(4, 0)).toBe('A5');
-    expect(getDataAtCell(5, 0)).toBe('A6');
-    expect(getDataAtCell(6, 0)).toBe('A7');
-  });
-
-  it('should trim rows after call enablePlugin method', () => {
-    const hot = handsontable({
-      data: getMultilineData(10, 10),
-      trimRows: [2, 6, 7],
-      width: 500,
-      height: 300
-    });
-    hot.getPlugin('hiddenRows').disablePlugin();
-    hot.getPlugin('hiddenRows').enablePlugin();
-    hot.render();
-
-    expect(getDataAtCell(0, 0)).toBe('A1');
-    expect(getDataAtCell(1, 0)).toBe('A2');
-    expect(getDataAtCell(2, 0)).toBe('A4');
-    expect(getDataAtCell(3, 0)).toBe('A5');
-    expect(getDataAtCell(4, 0)).toBe('A6');
-    expect(getDataAtCell(5, 0)).toBe('A9');
-    expect(getDataAtCell(6, 0)).toBe('A10');
-  });
-
-  it('should trim row after call trimRow method', () => {
-    const hot = handsontable({
-      data: getMultilineData(5, 10),
-      trimRows: true,
-      width: 500,
-      height: 300
-    });
-
-    expect(getDataAtCell(1, 0)).toBe('A2');
-
-    hot.getPlugin('trimRows').trimRow(1);
-    hot.render();
-
-    expect(getDataAtCell(1, 0)).toBe('A3');
-  });
-
-  it('should untrim row after call untrimRow method', () => {
-    const hot = handsontable({
-      data: getMultilineData(5, 10),
-      trimRows: [1],
-      width: 500,
-      height: 300
-    });
-
-    expect(getDataAtCell(1, 0)).toBe('A3');
-
-    hot.getPlugin('trimRows').untrimRow(1);
-    hot.render();
-
-    expect(getDataAtCell(1, 0)).toBe('A2');
-  });
-
-  it('should call hook after trim row', () => {
-    const callback = jasmine.createSpy();
-    const hot = handsontable({
-      data: getMultilineData(5, 10),
-      trimRows: true,
-      width: 500,
-      height: 300,
-    });
-
-    expect(callback).not.toHaveBeenCalled();
-
-    hot.addHook('afterTrimRow', callback);
-    hot.getPlugin('trimRows').trimRow(1);
-    hot.render();
-
-    expect(callback).toHaveBeenCalledWith([1], void 0, void 0, void 0, void 0, void 0);
-  });
-
-  it('should call hook after untrim row', () => {
-    const callback = jasmine.createSpy();
-    const hot = handsontable({
-      data: getMultilineData(5, 10),
-      trimRows: [1],
-      width: 500,
-      height: 300,
-    });
-
-    expect(callback).not.toHaveBeenCalled();
-
-    hot.addHook('afterUntrimRow', callback);
-    hot.getPlugin('trimRows').untrimRow(1);
-    hot.render();
-
-    expect(callback).toHaveBeenCalledWith([1], void 0, void 0, void 0, void 0, void 0);
-  });
-
-  it('should trim big data set', () => {
-    handsontable({
-      data: Handsontable.helper.createSpreadsheetData(1000, 5),
-      // leave first row and last 3 rows
-      trimRows: Array(...Array(996)).map((v, i) => i + 1),
-      width: 500,
-      height: 300
-    });
-
-    expect(getDataAtCell(0, 0)).toBe('A1');
-    expect(getDataAtCell(1, 0)).toBe('A998');
-    expect(getDataAtCell(2, 0)).toBe('A999');
-    expect(getDataAtCell(3, 0)).toBe('A1000');
-    expect(getDataAtCell(4, 0)).toBe(null);
-  });
-
-  it('should remove correct rows', () => {
-    handsontable({
-      data: getMultilineData(5, 10),
-      trimRows: [1],
-      width: 500,
-      height: 300
-    });
-
-    alter('remove_row', 0, 2);
-
-    expect(getDataAtCell(0, 0)).toBe('A4');
-    expect(getDataAtCell(1, 0)).toBe('A5');
-    expect(getDataAtCell(2, 0)).toBe(null);
-  });
-
-  it('should remove correct rows after inserting new ones', () => {
-    handsontable({
-      data: getMultilineData(6, 10),
-      trimRows: [1, 4],
-      width: 500,
-      height: 300
-    });
-
-    alter('insert_row', 1);
-    alter('insert_row', 3);
-    alter('remove_row', 0, 3);
-
-    expect(getDataAtCell(0, 0)).toBe(null);
-    expect(getDataAtCell(1, 0)).toBe('A4');
-    expect(getDataAtCell(2, 0)).toBe('A6');
-    expect(getDataAtCell(3, 0)).toBe(null);
-  });
-
-  it('should clear cache after loading new data by `loadData` function, when plugin `trimRows` is enabled #92', function(done) {
-    const hot = handsontable({
-      data: Handsontable.helper.createSpreadsheetData(5, 5),
-      trimRows: true
-    });
-
-    hot.loadData(Handsontable.helper.createSpreadsheetData(10, 10));
-
-    setTimeout(() => {
-      expect(this.$container.find('td').length).toEqual(100);
-      done();
-    }, 100);
-  });
-
-  it('should not affect `afterValidate` hook #11', (done) => {
-    const hot = handsontable({
-      data: Handsontable.helper.createSpreadsheetData(5, 2),
-      trimRows: true,
-      cells() {
-        return { type: 'numeric' };
-      }
-    });
-
-    hot.populateFromArray(5, 1, [
-      ['A1', 'A2'],
-      ['B1', 'B2'],
-      ['C1', 'C2'],
-      ['D1', 'D2'],
-      ['E1', 'E2'],
-    ]);
-
-    setTimeout(() => {
-      const $addedCell = $(getCell(5, 1));
-
-      expect($addedCell.hasClass('htInvalid')).toEqual(true);
-
-      done();
-    }, 100);
-  });
-
-  describe('copy-paste functionality', () => {
-    class DataTransferObject {
-      constructor() {
-        this.data = {
-          'text/plain': '',
-          'text/html': ''
-        };
-      }
-      getData(type) {
-        return this.data[type];
-      }
-      setData(type, value) {
-        this.data[type] = value;
-      }
-    }
-
-    function getClipboardEventMock() {
-      const event = {};
-      event.clipboardData = new DataTransferObject();
-      event.preventDefault = () => {};
-      return event;
-    }
-
-    it('should skip trimmed rows, while copying data', () => {
-      const hot = handsontable({
-        data: getMultilineData(10, 10),
-        trimRows: [1, 5, 6, 7, 8],
-        width: 500,
-        height: 300
-      });
-
-      const copyEvent = getClipboardEventMock('copy');
-      const plugin = hot.getPlugin('CopyPaste');
-
-      selectCell(0, 0, 4, 9);
-
-      plugin.setCopyableText();
-      plugin.onCopy(copyEvent);
-
-      /* eslint-disable no-tabs */
-      expect(copyEvent.clipboardData.getData('text/plain')).toEqual('A1	B1	"C1\n' +
-        'line"	D1	E1	F1	G1	H1	I1	J1\n' +
-        'A3	B3	C3	D3	E3	F3	G3	H3	I3	J3\n' +
-        'A4	B4	C4	D4	E4	F4	G4	H4	I4	J4\n' +
-        'A5	B5	C5	D5	E5	F5	G5	H5	I5	J5\n' +
-        'A10	B10	C10	D10	E10	F10	G10	H10	I10	J10'
-      );
-    });
-  });
-
-  describe('navigation', () => {
-    it('should ignore trimmed rows while navigating by arrow keys', () => {
-      handsontable({
-        data: getMultilineData(50, 10),
-        trimRows: [1, 5, 6, 7, 8],
-        width: 500,
-        height: 300
-      });
-
-      selectCell(0, 0, 0, 0);
-
-      expect(getValue()).toEqual('A1');
-
-      keyDownUp(Handsontable.helper.KEY_CODES.ARROW_DOWN);
-
-      expect(getValue()).toEqual('A3');
-
-      keyDownUp(Handsontable.helper.KEY_CODES.ARROW_DOWN);
-
-      expect(getValue()).toEqual('A4');
-
-      keyDownUp(Handsontable.helper.KEY_CODES.ARROW_DOWN);
-
-      expect(getValue()).toEqual('A5');
-
-      keyDownUp(Handsontable.helper.KEY_CODES.ARROW_DOWN);
-
-      expect(getValue()).toEqual('A10');
-    });
-  });
-
-  describe('column sorting', () => {
-    it('should remove correct rows after sorting', () => {
-      handsontable({
-        data: getMultilineData(5, 10),
-        columnSorting: {
-          initialConfig: {
-            column: 0,
-            sortOrder: 'desc'
-          }
-        },
-        trimRows: [1],
-        width: 500,
-        height: 300
-      });
-      alter('remove_row', 2, 1);
-
-      expect(getDataAtCell(0, 0)).toBe('A5');
-      expect(getDataAtCell(1, 0)).toBe('A4');
-      expect(getDataAtCell(2, 0)).toBe('A1');
-    });
-
-    it('should remove correct rows after insert new rows in sorted column', (done) => {
-      handsontable({
-        data: getMultilineData(5, 10),
-        colHeaders: true,
-        columnSorting: {
-          initialConfig: {
-            column: 0,
-            sortOrder: 'desc'
-          }
-        },
-        trimRows: [1],
-        width: 500,
-        height: 300
-      });
-
-      setTimeout(() => {
-        alter('insert_row', 2, 1);
-        getHtCore().find('th span.columnSorting:eq(2)').simulate('mousedown');
-        getHtCore().find('th span.columnSorting:eq(2)').simulate('mouseup');
-        getHtCore().find('th span.columnSorting:eq(2)').simulate('click');
-        alter('remove_row', 2, 1);
-
-        expect(getDataAtCell(0, 0)).toBe('A1');
-        expect(getDataAtCell(1, 0)).toBe('A3');
-        expect(getDataAtCell(2, 0)).toBe('A5');
-        expect(getDataAtCell(3, 0)).toBe(null);
-        done();
-      }, 100);
-    });
-
-    it('should remove correct rows after insert new rows in sorted column (multiple sort click)', (done) => {
-      handsontable({
-        data: getMultilineData(5, 10),
-        colHeaders: true,
-        columnSorting: {
-          initialConfig: {
-            column: 0,
-            sortOrder: 'desc'
-          }
-        },
-        trimRows: [1],
-        width: 500,
-        height: 300
-      });
-
-      setTimeout(() => {
-        alter('insert_row', 2, 1);
-        getHtCore().find('th span.columnSorting:eq(2)').simulate('mousedown');
-        getHtCore().find('th span.columnSorting:eq(2)').simulate('mouseup');
-        getHtCore().find('th span.columnSorting:eq(2)').simulate('click');
-        alter('insert_row', 0, 1);
-        getHtCore().find('th span.columnSorting:eq(2)').simulate('mousedown');
-        getHtCore().find('th span.columnSorting:eq(2)').simulate('mouseup');
-        getHtCore().find('th span.columnSorting:eq(2)').simulate('click');
-        alter('remove_row', 0, 3);
-
-        expect(getDataAtCell(0, 0)).toBe('A1');
-        expect(getDataAtCell(1, 0)).toBe(null);
-        expect(getDataAtCell(2, 0)).toBe(null);
-        expect(getDataAtCell(3, 0)).toBe(null);
-        done();
-      }, 100);
-    });
-  });
-
-  describe('maxRows option set', () => {
-    it('should return properly data after trimming', (done) => {
-      handsontable({
-        data: Handsontable.helper.createSpreadsheetData(10, 10),
-        maxRows: 3,
-        trimRows: [2, 3]
-      });
-
-      setTimeout(() => {
-        expect(getData().length).toEqual(3);
-        expect(getDataAtCell(2, 1)).toEqual('B5');
-        done();
-      }, 100);
-    });
-  });
-
-  describe('minRows option set', () => {
-    it('should not fill the table with empty rows (to the `minRows` limit), when editing rows in a table with trimmed rows', (done) => {
-      const hot = handsontable({
-        data: Handsontable.helper.createSpreadsheetData(10, 10),
-        minRows: 10,
-        trimRows: [1, 2, 3, 4, 5, 6, 7, 8, 9]
-      });
-
-      expect(hot.countRows()).toEqual(1);
-
-      hot.setDataAtCell(0, 0, 'test');
-
-      setTimeout(() => {
-        expect(hot.countRows()).toEqual(1);
-
-        done();
-      }, 100);
-    });
-  });
-
-  describe('minSpareRows option set', () => {
-    it('should not fill the table with empty rows (to the `minSpareRows` limit), when editing rows in a table with trimmed rows', (done) => {
-      const hot = handsontable({
-        data: Handsontable.helper.createSpreadsheetData(10, 10),
-        minSpareRows: 4,
-        trimRows: [1, 2, 3, 4, 5, 6, 7, 8, 9]
-      });
-
-      expect(hot.countRows()).toEqual(1);
-
-      hot.setDataAtCell(0, 0, 'test');
-
-      setTimeout(() => {
-        expect(hot.countRows()).toEqual(1);
-
-        done();
-      }, 100);
-    });
-  });
-
-  describe('updateSettings', () => {
-    it('should update list of trimmed rows when array of indexes is passed to the method - test no. 1', () => {
-      const hot = handsontable({
-        data: getMultilineData(10, 10),
-        trimRows: [2, 6, 7],
-        width: 500,
-        height: 300
-      });
-      hot.updateSettings({
-        trimRows: [1, 2, 3, 4, 5]
-      });
-
-      expect(getDataAtCell(0, 0)).toBe('A1');
-      expect(getDataAtCell(1, 0)).toBe('A7');
-      expect(getDataAtCell(2, 0)).toBe('A8');
-      expect(getDataAtCell(3, 0)).toBe('A9');
-      expect(getDataAtCell(4, 0)).toBe('A10');
-    });
-
-    it('should update list of trimmed rows when array of indexes is passed to the method - test no. 2', () => {
-      const hot = handsontable({
-        data: getMultilineData(10, 10),
-        trimRows: true,
-        width: 500,
-        height: 300
-      });
-
-      hot.getPlugin('trimRows').trimRows([2, 6, 7]);
-      hot.render();
-
-      hot.updateSettings({
-        trimRows: [1, 2, 3, 4, 5]
-      });
-
-      expect(getDataAtCell(0, 0)).toBe('A1');
-      expect(getDataAtCell(1, 0)).toBe('A7');
-      expect(getDataAtCell(2, 0)).toBe('A8');
-      expect(getDataAtCell(3, 0)).toBe('A9');
-      expect(getDataAtCell(4, 0)).toBe('A10');
-    });
-
-    it('should clear list of trimmed rows when empty array is passed to the method - test no. 1', function() {
-      const hot = handsontable({
-        data: getMultilineData(10, 10),
-        trimRows: [2, 6, 7],
-        width: 500,
-        height: 300
-      });
-
-      hot.updateSettings({
-        trimRows: []
-      });
-
-      expect(this.$container.find('td').length).toEqual(100);
-    });
-
-    it('should clear list of trimmed rows when empty array is passed to the method - test no. 2', function() {
-      const hot = handsontable({
-        data: getMultilineData(10, 10),
-        trimRows: true,
-        width: 500,
-        height: 300
-      });
-
-      hot.getPlugin('trimRows').trimRows([2, 6, 7]);
-      hot.render();
-
-      hot.updateSettings({
-        trimRows: []
-      });
-
-      expect(this.$container.find('td').length).toEqual(100);
-    });
-
-    it('should clear list of trimmed rows when handled setting object has key `trimRows` with value ' +
-      'set to `false` - test no. 1', function() {
-      const hot = handsontable({
-        data: getMultilineData(10, 10),
-        trimRows: [2, 6, 7],
-        width: 500,
-        height: 300
-      });
-
-      hot.updateSettings({
-        trimRows: false
-      });
-
-      expect(this.$container.find('td').length).toEqual(100);
-    });
-
-    it('should clear list of trimmed rows when handled setting object has key `trimRows` with value ' +
-      'set to `false` - test no. 2', function() {
-      const hot = handsontable({
-        data: getMultilineData(10, 10),
-        trimRows: true,
-        width: 500,
-        height: 300
-      });
-
-      hot.getPlugin('trimRows').trimRows([2, 6, 7]);
-      hot.render();
-
-      hot.updateSettings({
-        trimRows: false
-      });
-
-      expect(this.$container.find('td').length).toEqual(100);
-    });
-
-    it('shouldn\'t clear list of trimmed rows when handled setting object has key `trimRows` with value ' +
-      'set to `true` - test no. 1', () => {
-      const hot = handsontable({
-        data: getMultilineData(10, 10),
-        trimRows: [2, 6, 7],
-        width: 500,
-        height: 300
-      });
-
-      hot.updateSettings({
-        trimRows: true
-      });
-
-      expect(getData().length).toEqual(7);
-    });
-
-    it('shouldn\'t clear list of trimmed rows when handled setting object has key `trimRows` with value ' +
-      'set to `true` - test no. 2', () => {
-      const hot = handsontable({
-        data: getMultilineData(10, 10),
-        trimRows: true,
-        width: 500,
-        height: 300
-      });
-
-      hot.getPlugin('trimRows').trimRows([2, 6, 7]);
-      hot.render();
-
-      hot.updateSettings({
-        trimRows: true
-      });
-
-      expect(getData().length).toEqual(7);
-    });
-
-    it('shouldn\'t change list of trimmed rows when handled setting object don\'t have `trimRows` key - test no. 1', () => {
-      const hot = handsontable({
-        data: getMultilineData(10, 10),
-        trimRows: [2, 6, 7],
-        width: 500,
-        height: 300
-      });
-
-      hot.updateSettings({});
-
-      hot.render();
-
-      expect(getData().length).toEqual(7);
-    });
-
-    it('shouldn\'t change list of trimmed rows when handled setting object don\'t have `trimRows` key - test no. 2', () => {
-      const hot = handsontable({
-        data: getMultilineData(10, 10),
-        trimRows: true,
-        width: 500,
-        height: 300
-      });
-
-      hot.getPlugin('trimRows').trimRows([2, 6, 7]);
-      hot.render();
-      hot.updateSettings({});
-
-      expect(getData().length).toEqual(7);
-    });
-  });
-});

+ 0 - 322
handsontable/src/plugins/trimRows/trimRows.js

@@ -1,322 +0,0 @@
-import BasePlugin from '../_base';
-import { arrayEach } from '../../helpers/array';
-import { rangeEach } from '../../helpers/number';
-import { registerPlugin } from '../../plugins';
-import RowsMapper from './rowsMapper';
-
-/**
- * @plugin TrimRows
- * @pro
- *
- * @description
- * The plugin allows to trim certain rows. The trimming is achieved by applying the transformation algorithm to the data
- * transformation. In this case, when the row is trimmed it is not accessible using `getData*` methods thus the trimmed
- * data is not visible to other plugins.
- *
- * @example
- * ```js
- * const container = document.getElementById('example');
- * const hot = new Handsontable(container, {
- *   date: getData(),
- *   // hide selected rows on table initialization
- *   trimRows: [1, 2, 5]
- * });
- *
- * // access the trimRows plugin instance
- * const trimRowsPlugin = hot.getPlugin('trimRows');
- *
- * // hide single row
- * trimRowsPlugin.trimRow(1);
- *
- * // hide multiple rows
- * trimRowsPlugin.trimRow(1, 2, 9);
- *
- * // or as an array
- * trimRowsPlugin.trimRows([1, 2, 9]);
- *
- * // show single row
- * trimRowsPlugin.untrimRow(1);
- *
- * // show multiple rows
- * trimRowsPlugin.untrimRow(1, 2, 9);
- *
- * // or as an array
- * trimRowsPlugin.untrimRows([1, 2, 9]);
- *
- * // rerender table to see the changes
- * hot.render();
- * ```
- */
-class TrimRows extends BasePlugin {
-  constructor(hotInstance) {
-    super(hotInstance);
-    /**
-     * List of trimmed row indexes.
-     *
-     * @private
-     * @type {Array}
-     */
-    this.trimmedRows = [];
-    /**
-     * List of last removed row indexes.
-     *
-     * @private
-     * @type {Array}
-     */
-    this.removedRows = [];
-    /**
-     * Object containing visual row indexes mapped to data source indexes.
-     *
-     * @private
-     * @type {RowsMapper}
-     */
-    this.rowsMapper = new RowsMapper(this);
-  }
-
-  /**
-   * Checks if the plugin is enabled in the handsontable settings. This method is executed in {@link Hooks#beforeInit}
-   * hook and if it returns `true` than the {@link AutoRowSize#enablePlugin} method is called.
-   *
-   * @returns {Boolean}
-   */
-  isEnabled() {
-    return !!this.hot.getSettings().trimRows;
-  }
-
-  /**
-   * Enables the plugin functionality for this Handsontable instance.
-   */
-  enablePlugin() {
-    if (this.enabled) {
-      return;
-    }
-    const settings = this.hot.getSettings().trimRows;
-
-    if (Array.isArray(settings)) {
-      this.trimmedRows = settings;
-    }
-    this.rowsMapper.createMap(this.hot.countSourceRows());
-
-    this.addHook('modifyRow', (row, source) => this.onModifyRow(row, source));
-    this.addHook('unmodifyRow', (row, source) => this.onUnmodifyRow(row, source));
-    this.addHook('beforeCreateRow', (index, amount, source) => this.onBeforeCreateRow(index, amount, source));
-    this.addHook('afterCreateRow', (index, amount) => this.onAfterCreateRow(index, amount));
-    this.addHook('beforeRemoveRow', (index, amount) => this.onBeforeRemoveRow(index, amount));
-    this.addHook('afterRemoveRow', () => this.onAfterRemoveRow());
-    this.addHook('afterLoadData', firstRun => this.onAfterLoadData(firstRun));
-
-    super.enablePlugin();
-  }
-
-  /**
-   * Updates the plugin state. This method is executed when {@link Core#updateSettings} is invoked.
-   */
-  updatePlugin() {
-    const settings = this.hot.getSettings().trimRows;
-
-    if (Array.isArray(settings)) {
-      this.disablePlugin();
-      this.enablePlugin();
-    }
-
-    super.updatePlugin();
-  }
-
-  /**
-   * Disables the plugin functionality for this Handsontable instance.
-   */
-  disablePlugin() {
-    this.trimmedRows = [];
-    this.removedRows.length = 0;
-    this.rowsMapper.clearMap();
-    super.disablePlugin();
-  }
-
-  /**
-   * Trims the rows provided in the array.
-   *
-   * @param {Number[]} rows Array of physical row indexes.
-   * @fires Hooks#skipLengthCache
-   * @fires Hooks#afterTrimRow
-   */
-  trimRows(rows) {
-    arrayEach(rows, (row) => {
-      const physicalRow = parseInt(row, 10);
-
-      if (!this.isTrimmed(physicalRow)) {
-        this.trimmedRows.push(physicalRow);
-      }
-    });
-
-    this.hot.runHooks('skipLengthCache', 100);
-    this.rowsMapper.createMap(this.hot.countSourceRows());
-    this.hot.runHooks('afterTrimRow', rows);
-  }
-
-  /**
-   * Trims the row provided as physical row index (counting from 0).
-   *
-   * @param {...Number} row Physical row index.
-   */
-  trimRow(...row) {
-    this.trimRows(row);
-  }
-
-  /**
-   * Untrims the rows provided in the array.
-   *
-   * @param {Number[]} rows Array of physical row indexes.
-   * @fires Hooks#skipLengthCache
-   * @fires Hooks#afterUntrimRow
-   */
-  untrimRows(rows) {
-    arrayEach(rows, (row) => {
-      const physicalRow = parseInt(row, 10);
-
-      if (this.isTrimmed(physicalRow)) {
-        this.trimmedRows.splice(this.trimmedRows.indexOf(physicalRow), 1);
-      }
-    });
-
-    this.hot.runHooks('skipLengthCache', 100);
-    this.rowsMapper.createMap(this.hot.countSourceRows());
-    this.hot.runHooks('afterUntrimRow', rows);
-  }
-
-  /**
-   * Untrims the row provided as row index (counting from 0).
-   *
-   * @param {...Number} row Physical row index.
-   */
-  untrimRow(...row) {
-    this.untrimRows(row);
-  }
-
-  /**
-   * Checks if given physical row is hidden.
-   *
-   * @returns {Boolean}
-   */
-  isTrimmed(row) {
-    return this.trimmedRows.indexOf(row) > -1;
-  }
-
-  /**
-   * Untrims all trimmed rows.
-   */
-  untrimAll() {
-    this.untrimRows([].concat(this.trimmedRows));
-  }
-
-  /**
-   * On modify row listener.
-   *
-   * @private
-   * @param {Number} row Visual row index.
-   * @param {String} source Source name.
-   * @returns {Number|null}
-   */
-  onModifyRow(row, source) {
-    let physicalRow = row;
-
-    if (source !== this.pluginName) {
-      physicalRow = this.rowsMapper.getValueByIndex(physicalRow);
-    }
-
-    return physicalRow;
-  }
-
-  /**
-   * On unmodifyRow listener.
-   *
-   * @private
-   * @param {Number} row Physical row index.
-   * @param {String} source Source name.
-   * @returns {Number|null}
-   */
-  onUnmodifyRow(row, source) {
-    let visualRow = row;
-
-    if (source !== this.pluginName) {
-      visualRow = this.rowsMapper.getIndexByValue(visualRow);
-    }
-
-    return visualRow;
-  }
-
-  /**
-   * `beforeCreateRow` hook callback.
-   *
-   * @private
-   * @param {Number} index Index of the newly created row.
-   * @param {Number} amount Amount of created rows.
-   * @param {String} source Source of the change.
-   */
-  onBeforeCreateRow(index, amount, source) {
-    return !(this.isEnabled() && this.trimmedRows.length > 0 && source === 'auto');
-  }
-
-  /**
-   * On after create row listener.
-   *
-   * @private
-   * @param {Number} index Visual row index.
-   * @param {Number} amount Defines how many rows removed.
-   */
-  onAfterCreateRow(index, amount) {
-    this.rowsMapper.shiftItems(index, amount);
-  }
-
-  /**
-   * On before remove row listener.
-   *
-   * @private
-   * @param {Number} index Visual row index.
-   * @param {Number} amount Defines how many rows removed.
-   *
-   * @fires Hooks#modifyRow
-   */
-  onBeforeRemoveRow(index, amount) {
-    this.removedRows.length = 0;
-
-    if (index !== false) {
-      // Collect physical row index.
-      rangeEach(index, index + amount - 1, (removedIndex) => {
-        this.removedRows.push(this.hot.runHooks('modifyRow', removedIndex, this.pluginName));
-      });
-    }
-  }
-
-  /**
-   * On after remove row listener.
-   *
-   * @private
-   */
-  onAfterRemoveRow() {
-    this.rowsMapper.unshiftItems(this.removedRows);
-  }
-
-  /**
-   * On after load data listener.
-   *
-   * @private
-   * @param {Boolean} firstRun Indicates if hook was fired while Handsontable initialization.
-   */
-  onAfterLoadData(firstRun) {
-    if (!firstRun) {
-      this.rowsMapper.createMap(this.hot.countSourceRows());
-    }
-  }
-
-  /**
-   * Destroys the plugin instance.
-   */
-  destroy() {
-    this.rowsMapper.destroy();
-    super.destroy();
-  }
-}
-
-registerPlugin('trimRows', TrimRows);
-
-export default TrimRows;

+ 2 - 2
types/src/interface/bill.ts

@@ -1,6 +1,6 @@
 import { BRType } from './base';
 
-export interface IBillsItem {
+export interface IBill {
   ID: string;
   parentID: string;
   seq: number;
@@ -14,7 +14,7 @@ export interface IBillsItem {
 
 export interface IBills {
   projectID: string;
-  bills: IBillsItem[];
+  bills: IBill[];
 }
 
 export interface IJobContent {

+ 1 - 1
types/src/interface/calculation.ts

@@ -51,7 +51,7 @@ export interface IStdCalcProgram {
   templates: ICalcTemplate[];
 }
 
-export interface ICalcProgram {
+export interface ICalcProgramFile {
   ID: string;
   projectID: string;
   name: string;

+ 1 - 1
types/src/interface/labourCoe.ts

@@ -19,7 +19,7 @@ export interface ILabourCoeItem extends ITreeScm {
   coe: number;
 }
 
-export interface ILabourCoe {
+export interface ILabourCoeFile {
   ID: string;
   projectID: string;
   name: string;

+ 2 - 2
types/src/interface/project.ts

@@ -1,4 +1,4 @@
-import { ILabourCoe } from './labourCoe';
+import { ILabourCoeFile } from './labourCoe';
 import { IIncreaseSetting } from './increaseFee';
 import { ITreeScm, IFileRef, DeleteEnum, INumFileRef } from './base';
 import { ICalcOption, ITenderSetting } from './calculation';
@@ -191,7 +191,7 @@ export interface IProperty {
   progressiveInterval?: IProgressiveInterval[]; // 累进区间
   gljAdjustType?: GLJAdjustType; // 承包人材料调整类型
   cptIllustration?: string; // 编制说明
-  labourCoeFile?: ILabourCoe; // 人工系数文件
+  labourCoeFile?: ILabourCoeFile; // 人工系数文件
   engineerInfos?: IInfoItem[];
   engineerFeatures?: IEngineerFeature[];
   materials?: IMaterialIndex[];

+ 5 - 5
types/src/interface/ration.ts

@@ -10,8 +10,8 @@ import {
 
 export interface IStdRationChapter {
   rationRepId: number; // 标准库的属性
-  ID: number; // 补充库的直接用新结构,所以有两种类型
-  parentID: number;
+  ID: string; // 补充库的直接用新结构,所以有两种类型
+  parentID: string;
   seq: number;
   name: string;
   explanation?: string; // 说明
@@ -22,7 +22,7 @@ export interface IStdRationChapter {
 
 export interface ICptRationChapter {
   name: string;
-  ID: { type: string; index: true };
+  ID: string;
   seq: number;
   parentID: string;
   // 以下预留数据,以后开放可用
@@ -317,7 +317,7 @@ export interface IRationInstall {
   unifiedSetting: boolean; // 0:false 1:true  按统一设置
 }
 
-export interface IRationItem {
+export interface IRation {
   ID: string;
   parentID: string;
   seq: number;
@@ -382,5 +382,5 @@ export interface IRationItem {
 export interface IRations {
   projectID: string;
   index: number;
-  rations: IRationItem[];
+  rations: IRation[];
 }