{"size": 11258, "ext": "js", "max_stars_repo_path": "src/plugins/nestedRows/utils/rowMoveController.js", "max_stars_repo_name": "onlyhouse1980/handsontable", "max_stars_repo_stars_event_min_datetime": null, "max_stars_repo_stars_event_max_datetime": null, "max_issues_repo_path": "src/plugins/nestedRows/utils/rowMoveController.js", "max_issues_repo_name": "onlyhouse1980/handsontable", "max_issues_repo_issues_event_min_datetime": null, "max_issues_repo_issues_event_max_datetime": null, "max_forks_repo_path": "src/plugins/nestedRows/utils/rowMoveController.js", "max_forks_repo_name": "onlyhouse1980/handsontable", "max_forks_repo_forks_event_min_datetime": null, "max_forks_repo_forks_event_max_datetime": null}
{"max_stars_count": null, "max_issues_count": null, "max_forks_count": null, "avg_line_length": 35.6265822785, "max_line_length": 118, "alphanum_fraction": 0.6927518209}
import { isUndefined } from '../../../helpers/mixed';
import { warn } from '../../../helpers/console';
import { toSingleLine } from '../../../helpers/templateLiteralTag';
import { CellCoords } from '../../../3rdparty/walkontable/src';

/**
 * Helper class for the row-move-related operations.
 *
 * @class RowMoveController
 * @plugin NestedRows
 * @private
 */
export default class RowMoveController {
  constructor(plugin) {
    /**
     * Reference to the Nested Rows plugin instance.
     *
     * @type {NestedRows}
     */
    this.plugin = plugin;
    /**
     * Reference to the Handsontable instance.
     *
     * @type {Handsontable.Core}
     */
    this.hot = plugin.hot;
    /**
     * Reference to the Data Manager class instance.
     *
     * @type {DataManager}
     */
    this.dataManager = plugin.dataManager;
    /**
     * Reference to the Collapsing UI class instance.
     *
     * @type {CollapsingUI}
     */
    this.collapsingUI = plugin.collapsingUI;
  }

  /**
   * `beforeRowMove` hook callback.
   *
   * @param {Array} rows Array of visual row indexes to be moved.
   * @param {number} finalIndex Visual row index, being a start index for the moved rows. Points to where the elements
   *   will be placed after the moving action. To check the visualization of the final index, please take a look at
   *   [documentation](/docs/demo-moving.html).
   * @param {undefined|number} dropIndex Visual row index, being a drop index for the moved rows. Points to where we
   *   are going to drop the moved elements. To check visualization of drop index please take a look at
   *   [documentation](/docs/demo-moving.html).
   * @param {boolean} movePossible Indicates if it's possible to move rows to the desired position.
   * @fires Hooks#afterRowMove
   * @returns {boolean}
   */
  onBeforeRowMove(rows, finalIndex, dropIndex, movePossible) {
    const improperUsage = this.displayAPICompatibilityWarning({ rows, finalIndex, dropIndex, movePossible });

    if (improperUsage) {
      return false;
    }

    this.movedToCollapsed = false;
    const dropToLastRow = dropIndex === this.hot.countRows();
    const physicalDropIndex = dropToLastRow ?
      this.hot.countSourceRows() :
      this.dataManager.translateTrimmedRow(dropIndex);
    let allowMove = true;
    const physicalStartIndexes = rows.map((rowIndex) => {
      // Don't do the logic for the rest of the rows, as it's bound to fail anyway.
      if (!allowMove) {
        return false;
      }

      const physicalRowIndex = this.dataManager.translateTrimmedRow(rowIndex);

      allowMove = this.shouldAllowMoving(physicalRowIndex, physicalDropIndex);

      return physicalRowIndex;
    });
    const willDataChange = physicalStartIndexes.indexOf(physicalDropIndex) === -1;

    if (!allowMove || !willDataChange) {
      return false;
    }

    const baseParent = this.getBaseParent(physicalStartIndexes);
    const targetParent = this.getTargetParent(dropToLastRow, physicalDropIndex);
    const sameParent = baseParent === targetParent;

    this.movedToCollapsed = this.collapsingUI.areChildrenCollapsed(targetParent);

    // Stash the current state of collapsed rows
    this.collapsingUI.collapsedRowsStash.stash();

    this.shiftCollapsibleParentsLocations(physicalStartIndexes, physicalDropIndex, sameParent);

    this.moveRows(physicalStartIndexes, physicalDropIndex, targetParent);

    this.dataManager.updateWithData(this.dataManager.getRawSourceData());

    this.moveCellsMeta(physicalStartIndexes, physicalDropIndex);

    this.collapsingUI.collapsedRowsStash.applyStash(false);

    // TODO: Trying to mock real work of the `ManualRowMove` plugin. It was blocked by returning `false` below.
    this.hot.runHooks('afterRowMove',
      rows, finalIndex, dropIndex, movePossible, movePossible && this.isRowOrderChanged(rows, finalIndex));

    // Not necessary - added to keep compatibility with other plugins (namely: columnSummary).
    this.hot.render();

    this.selectCells(rows, dropIndex);

    return false;
  }

  /**
   * Display a `dragRows`/`moveRows` method compatibility warning if needed.
   *
   * @param {object} beforeMoveRowHookArgs A set of arguments from the `beforeMoveRow` hook.
   * @returns {boolean} `true` if is a result of an improper usage of the moving API.
   */
  displayAPICompatibilityWarning(beforeMoveRowHookArgs) {
    const {
      rows,
      finalIndex,
      dropIndex,
      movePossible
    } = beforeMoveRowHookArgs;
    let shouldTerminate = false;

    if (isUndefined(dropIndex)) {
      warn(toSingleLine`Since version 8.0.0 of the Handsontable the 'moveRows' method isn't used for moving rows\x20
      when the NestedRows plugin is enabled. Please use the 'dragRows' method instead.`);

      // TODO: Trying to mock real work of the `ManualRowMove` plugin. It was blocked by returning `false` below.
      this.hot.runHooks('afterRowMove', rows, finalIndex, dropIndex, movePossible, false);

      shouldTerminate = true;
    }

    return shouldTerminate;
  }

  /**
   * Check if the moving action should be allowed.
   *
   * @param {number} physicalRowIndex Physical start row index.
   * @param {number} physicalDropIndex Physical drop index.
   * @returns {boolean} `true` if it should continue with the moving action.
   */
  shouldAllowMoving(physicalRowIndex, physicalDropIndex) {
    /*
       We can't move rows when any of them is:
       - a parent
       - a top-level element
       - is being moved to the top level
       - is being moved to the position of any of the moved rows (not changing position)
    */

    return !(
      this.dataManager.isParent(physicalRowIndex) ||
      this.dataManager.isRowHighestLevel(physicalRowIndex) ||
      physicalRowIndex === physicalDropIndex ||
      physicalDropIndex === 0
    );
  }

  /**
   * Get the base row parent.
   *
   * @param {number} physicalStartIndexes Physical start row index.
   * @returns {object|null} The base row parent.
   */
  getBaseParent(physicalStartIndexes) {
    return this.dataManager.getRowParent(physicalStartIndexes[0]);
  }

  /**
   * Get the target row parent.
   *
   * @param {boolean} dropToLastRow `true` if the row is moved to the last row of the table.
   * @param {number} physicalDropIndex Physical drop row index.
   * @returns {object|null} The target row parent.
   */
  getTargetParent(dropToLastRow, physicalDropIndex) {
    let targetParent = this.dataManager.getRowParent(dropToLastRow ? physicalDropIndex - 1 : physicalDropIndex);

    // If we try to move an element to the place of a top-level parent, snap the element to the previous top-level
    // parent's children instead
    if (targetParent === null || targetParent === void 0) {
      targetParent = this.dataManager.getRowParent(physicalDropIndex - 1);
    }

    return targetParent;
  }

  /**
   * Shift the cached collapsible rows position according to the move action.
   *
   * @param {number[]} physicalStartIndexes Physical start row indexes.
   * @param {number} physicalDropIndex Physical drop index.
   * @param {boolean} sameParent `true` if the row's being moved between siblings of the same parent.
   */
  shiftCollapsibleParentsLocations(physicalStartIndexes, physicalDropIndex, sameParent) {
    if (!sameParent) {
      if (Math.max(...physicalStartIndexes) <= physicalDropIndex) {
        this.collapsingUI.collapsedRowsStash.shiftStash(physicalStartIndexes[0], physicalDropIndex,
          (-1) * physicalStartIndexes.length);

      } else {
        this.collapsingUI.collapsedRowsStash.shiftStash(physicalDropIndex, physicalStartIndexes[0],
          physicalStartIndexes.length);
      }
    }
  }

  /**
   * Move the rows at the provided coordinates.
   *
   * @param {number[]} physicalStartIndexes Physical indexes of the rows about to be moved.
   * @param {number} physicalDropIndex Physical drop index.
   * @param {object} targetParent Parent of the destination row.
   */
  moveRows(physicalStartIndexes, physicalDropIndex, targetParent) {
    const moveToLastChild = physicalDropIndex === this.dataManager.getRowIndex(targetParent) +
      this.dataManager.countChildren(targetParent) + 1;

    physicalStartIndexes.forEach((physicalStartIndex) => {
      this.dataManager.moveRow(physicalStartIndex, physicalDropIndex, this.movedToCollapsed, moveToLastChild, true);
    });
  }

  /**
   * Move the cell meta for multiple rows.
   *
   * @param {number[]} baseIndexes Array of indexes for the rows being moved.
   * @param {number} targetIndex Index of the destination of the move.
   */
  moveCellsMeta(baseIndexes, targetIndex) {
    const rowsOfMeta = [];
    const movingDown = Math.max(...baseIndexes) < targetIndex;

    baseIndexes.forEach((baseIndex) => {
      rowsOfMeta.push(this.hot.getCellMetaAtRow(baseIndex));
    });

    this.hot.spliceCellsMeta(baseIndexes[0], baseIndexes.length);

    this.hot.spliceCellsMeta(targetIndex - (movingDown ? rowsOfMeta.length : 0), 0, ...rowsOfMeta);
  }

  /**
   * Select cells after the move.
   *
   * @param {Array} rows Array of visual row indexes to be moved.
   * @param {undefined|number} dropIndex Visual row index, being a drop index for the moved rows. Points to where we
   *   are going to drop the moved elements. To check visualization of drop index please take a look at
   *   [documentation](/docs/demo-moving.html).
   */
  selectCells(rows, dropIndex) {
    const rowsLen = rows.length;
    let startRow = 0;
    let endRow = 0;
    let selection = null;
    let lastColIndex = null;

    if (this.movedToCollapsed) {
      let physicalDropIndex = null;

      if (rows[rowsLen - 1] < dropIndex) {
        physicalDropIndex = this.dataManager.translateTrimmedRow(dropIndex - rowsLen);

      } else {
        physicalDropIndex = this.dataManager.translateTrimmedRow(dropIndex);
      }

      const parentObject = this.dataManager.getRowParent(
        physicalDropIndex === null ? this.hot.countSourceRows() - 1 : physicalDropIndex - 1
      );
      const parentIndex = this.dataManager.getRowIndex(parentObject);

      startRow = this.dataManager.untranslateTrimmedRow(parentIndex);
      endRow = startRow;

    } else if (rows[rowsLen - 1] < dropIndex) {
      endRow = dropIndex - 1;
      startRow = endRow - rowsLen + 1;

    } else {
      startRow = dropIndex;
      endRow = startRow + rowsLen - 1;
    }

    selection = this.hot.selection;
    lastColIndex = this.hot.countCols() - 1;

    selection.setRangeStart(new CellCoords(startRow, 0));
    selection.setRangeEnd(new CellCoords(endRow, lastColIndex), true);
  }

  // TODO: Reimplementation of function which is inside the `ManualRowMove` plugin.
  /**
   * Indicates if order of rows was changed.
   *
   * @param {Array} movedRows Array of visual row indexes to be moved.
   * @param {number} finalIndex Visual row index, being a start index for the moved rows. Points to where the elements
   *   will be placed after the moving action. To check the visualization of the final index, please take a look at
   *   [documentation](/docs/demo-moving.html).
   * @returns {boolean}
   */
  isRowOrderChanged(movedRows, finalIndex) {
    return movedRows.some((row, nrOfMovedElement) => row - nrOfMovedElement !== finalIndex);
  }
}
