All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.src.vaadin-grid-column-reordering-mixin.js Maven / Gradle / Ivy

Go to download

A free, flexible and high-quality Web Component for showing large amounts of tabular data

There is a newer version: 24.6.0
Show newest version
/**
 * @license
 * Copyright (c) 2016 - 2024 Vaadin Ltd.
 * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
 */
import { isTouch } from '@vaadin/component-base/src/browser-utils.js';
import { addListener } from '@vaadin/component-base/src/gestures.js';
import { iterateChildren, updateColumnOrders } from './vaadin-grid-helpers.js';

/**
 * @polymerMixin
 */
export const ColumnReorderingMixin = (superClass) =>
  class ColumnReorderingMixin extends superClass {
    static get properties() {
      return {
        /**
         * Set to true to allow column reordering.
         * @attr {boolean} column-reordering-allowed
         * @type {boolean}
         */
        columnReorderingAllowed: {
          type: Boolean,
          value: false,
        },

        /** @private */
        _orderBaseScope: {
          type: Number,
          value: 10000000,
        },
      };
    }

    static get observers() {
      return ['_updateOrders(_columnTree)'];
    }

    /** @protected */
    ready() {
      super.ready();
      addListener(this, 'track', this._onTrackEvent);
      this._reorderGhost = this.shadowRoot.querySelector('[part="reorder-ghost"]');

      this.addEventListener('touchstart', this._onTouchStart.bind(this));
      this.addEventListener('touchmove', this._onTouchMove.bind(this));
      this.addEventListener('touchend', this._onTouchEnd.bind(this));
      this.addEventListener('contextmenu', this._onContextMenu.bind(this));
    }

    /** @private */
    _onContextMenu(e) {
      if (this.hasAttribute('reordering')) {
        e.preventDefault();

        // A contextmenu event is fired on mobile Chrome on long-press
        // (which should start reordering). Don't end the reorder on touch devices.
        if (!isTouch) {
          // Context menu cancels the track gesture on desktop without firing an end event.
          // End the reorder manually.
          this._onTrackEnd();
        }
      }
    }

    /** @private */
    _onTouchStart(e) {
      // Touch event, delay activation by 100ms
      this._startTouchReorderTimeout = setTimeout(() => {
        this._onTrackStart({
          detail: {
            x: e.touches[0].clientX,
            y: e.touches[0].clientY,
          },
        });
      }, 100);
    }

    /** @private */
    _onTouchMove(e) {
      if (this._draggedColumn) {
        e.preventDefault();
      }
      clearTimeout(this._startTouchReorderTimeout);
    }

    /** @private */
    _onTouchEnd() {
      clearTimeout(this._startTouchReorderTimeout);
      this._onTrackEnd();
    }

    /** @private */
    _onTrackEvent(e) {
      if (e.detail.state === 'start') {
        const path = e.composedPath();
        const headerCell = path[path.indexOf(this.$.header) - 2];
        if (!headerCell || !headerCell._content) {
          // Not a header column
          return;
        }

        if (headerCell._content.contains(this.getRootNode().activeElement)) {
          // Something was focused inside the cell
          return;
        }

        if (this.$.scroller.hasAttribute('column-resizing')) {
          // Resizing is in progress
          return;
        }

        if (!this._touchDevice) {
          // Not a touch device
          this._onTrackStart(e);
        }
      } else if (e.detail.state === 'track') {
        this._onTrack(e);
      } else if (e.detail.state === 'end') {
        this._onTrackEnd(e);
      }
    }

    /** @private */
    _onTrackStart(e) {
      if (!this.columnReorderingAllowed) {
        return;
      }

      // Cancel reordering if there are draggable nodes on the event path
      const path = e.composedPath && e.composedPath();
      if (path && path.some((node) => node.hasAttribute && node.hasAttribute('draggable'))) {
        return;
      }

      const headerCell = this._cellFromPoint(e.detail.x, e.detail.y);
      if (!headerCell || !headerCell.getAttribute('part').includes('header-cell')) {
        return;
      }

      this.toggleAttribute('reordering', true);
      this._draggedColumn = headerCell._column;
      while (this._draggedColumn.parentElement.childElementCount === 1) {
        // This is the only column in the group, drag the whole group instead
        this._draggedColumn = this._draggedColumn.parentElement;
      }
      this._setSiblingsReorderStatus(this._draggedColumn, 'allowed');
      this._draggedColumn._reorderStatus = 'dragging';

      this._updateGhost(headerCell);
      this._reorderGhost.style.visibility = 'visible';
      this._updateGhostPosition(e.detail.x, this._touchDevice ? e.detail.y - 50 : e.detail.y);
      this._autoScroller();
    }

    /** @private */
    _onTrack(e) {
      if (!this._draggedColumn) {
        // Reordering didn't start. Skip this event.
        return;
      }

      const targetCell = this._cellFromPoint(e.detail.x, e.detail.y);
      if (!targetCell) {
        return;
      }

      const targetColumn = this._getTargetColumn(targetCell, this._draggedColumn);
      if (
        this._isSwapAllowed(this._draggedColumn, targetColumn) &&
        this._isSwappableByPosition(targetColumn, e.detail.x)
      ) {
        // Get the header level of the target column (and the dragged column)
        const columnTreeLevel = this._columnTree.findIndex((level) => level.includes(targetColumn));
        // Get the columns on that level in visual order
        const levelColumnsInOrder = this._getColumnsInOrder(columnTreeLevel);

        // Index of the column being dragged
        const startIndex = levelColumnsInOrder.indexOf(this._draggedColumn);
        // Index of the column being dragged over
        const endIndex = levelColumnsInOrder.indexOf(targetColumn);

        // Direction of iteration
        const direction = startIndex < endIndex ? 1 : -1;

        // Iteratively swap all the columns from the dragged column to the target column
        for (let i = startIndex; i !== endIndex; i += direction) {
          this._swapColumnOrders(this._draggedColumn, levelColumnsInOrder[i + direction]);
        }
      }

      this._updateGhostPosition(e.detail.x, this._touchDevice ? e.detail.y - 50 : e.detail.y);
      this._lastDragClientX = e.detail.x;
    }

    /** @private */
    _onTrackEnd() {
      if (!this._draggedColumn) {
        // Reordering didn't start. Skip this event.
        return;
      }

      this.toggleAttribute('reordering', false);
      this._draggedColumn._reorderStatus = '';
      this._setSiblingsReorderStatus(this._draggedColumn, '');
      this._draggedColumn = null;
      this._lastDragClientX = null;
      this._reorderGhost.style.visibility = 'hidden';

      this.dispatchEvent(
        new CustomEvent('column-reorder', {
          detail: {
            columns: this._getColumnsInOrder(),
          },
        }),
      );
    }

    /**
     * Returns the columns (or column groups) on the specified header level in visual order.
     * By default, the bottom level is used.
     *
     * @return {!Array}
     * @protected
     */
    _getColumnsInOrder(headerLevel = this._columnTree.length - 1) {
      return this._columnTree[headerLevel].filter((c) => !c.hidden).sort((b, a) => b._order - a._order);
    }

    /**
     * @param {number} x
     * @param {number} y
     * @return {HTMLElement | undefined}
     * @protected
     */
    _cellFromPoint(x = 0, y = 0) {
      if (!this._draggedColumn) {
        this.$.scroller.toggleAttribute('no-content-pointer-events', true);
      }
      const elementFromPoint = this.shadowRoot.elementFromPoint(x, y);
      this.$.scroller.toggleAttribute('no-content-pointer-events', false);

      return this._getCellFromElement(elementFromPoint);
    }

    /** @private */
    _getCellFromElement(element) {
      if (element) {
        // Check if element is a cell
        if (element._column) {
          return element;
        }
        // Check if element is the cell of a focus button mode column
        const { parentElement } = element;
        if (parentElement && parentElement._focusButton === element) {
          return parentElement;
        }
      }
      return null;
    }

    /**
     * @param {number} eventClientX
     * @param {number} eventClientY
     * @protected
     */
    _updateGhostPosition(eventClientX, eventClientY) {
      const ghostRect = this._reorderGhost.getBoundingClientRect();
      // // This is where we want to position the ghost
      const targetLeft = eventClientX - ghostRect.width / 2;
      const targetTop = eventClientY - ghostRect.height / 2;
      // Current position
      const _left = parseInt(this._reorderGhost._left || 0);
      const _top = parseInt(this._reorderGhost._top || 0);
      // Reposition the ghost
      this._reorderGhost._left = _left - (ghostRect.left - targetLeft);
      this._reorderGhost._top = _top - (ghostRect.top - targetTop);
      this._reorderGhost.style.transform = `translate(${this._reorderGhost._left}px, ${this._reorderGhost._top}px)`;
    }

    /**
     * @param {!HTMLElement} cell
     * @return {!HTMLElement}
     * @protected
     */
    _updateGhost(cell) {
      const ghost = this._reorderGhost;
      ghost.textContent = cell._content.innerText;
      const style = window.getComputedStyle(cell);
      [
        'boxSizing',
        'display',
        'width',
        'height',
        'background',
        'alignItems',
        'padding',
        'border',
        'flex-direction',
        'overflow',
      ].forEach((propertyName) => {
        ghost.style[propertyName] = style[propertyName];
      });
      return ghost;
    }

    /** @private */
    _updateOrders(columnTree) {
      if (columnTree === undefined) {
        return;
      }

      // Reset all column orders
      columnTree[0].forEach((column) => {
        column._order = 0;
      });
      // Set order numbers to top-level columns
      updateColumnOrders(columnTree[0], this._orderBaseScope, 0);
    }

    /**
     * @param {!GridColumn} column
     * @param {string} status
     * @protected
     */
    _setSiblingsReorderStatus(column, status) {
      iterateChildren(column.parentNode, (sibling) => {
        if (/column/u.test(sibling.localName) && this._isSwapAllowed(sibling, column)) {
          sibling._reorderStatus = status;
        }
      });
    }

    /** @protected */
    _autoScroller() {
      if (this._lastDragClientX) {
        const rightDiff = this._lastDragClientX - this.getBoundingClientRect().right + 50;
        const leftDiff = this.getBoundingClientRect().left - this._lastDragClientX + 50;

        if (rightDiff > 0) {
          this.$.table.scrollLeft += rightDiff / 10;
        } else if (leftDiff > 0) {
          this.$.table.scrollLeft -= leftDiff / 10;
        }
      }

      if (this._draggedColumn) {
        setTimeout(() => this._autoScroller(), 10);
      }
    }

    /**
     * @param {GridColumn | undefined} column1
     * @param {GridColumn | undefined} column2
     * @return {boolean | undefined}
     * @protected
     */
    _isSwapAllowed(column1, column2) {
      if (column1 && column2) {
        const differentColumns = column1 !== column2;
        const sameParent = column1.parentElement === column2.parentElement;
        const sameFrozen =
          (column1.frozen && column2.frozen) || // Both columns are frozen
          (column1.frozenToEnd && column2.frozenToEnd) || // Both columns are frozen to end
          (!column1.frozen && !column1.frozenToEnd && !column2.frozen && !column2.frozenToEnd);
        return differentColumns && sameParent && sameFrozen;
      }
    }

    /**
     * @param {!GridColumn} targetColumn
     * @param {number} clientX
     * @return {boolean}
     * @protected
     */
    _isSwappableByPosition(targetColumn, clientX) {
      const targetCell = Array.from(this.$.header.querySelectorAll('tr:not([hidden]) [part~="cell"]')).find((cell) =>
        targetColumn.contains(cell._column),
      );
      const sourceCellRect = this.$.header
        .querySelector('tr:not([hidden]) [reorder-status=dragging]')
        .getBoundingClientRect();
      const targetRect = targetCell.getBoundingClientRect();
      if (targetRect.left > sourceCellRect.left) {
        return clientX > targetRect.right - sourceCellRect.width;
      }
      return clientX < targetRect.left + sourceCellRect.width;
    }

    /**
     * @param {!GridColumn} column1
     * @param {!GridColumn} column2
     * @protected
     */
    _swapColumnOrders(column1, column2) {
      [column1._order, column2._order] = [column2._order, column1._order];
      this._debounceUpdateFrozenColumn();
      this._updateFirstAndLastColumn();
    }

    /**
     * @param {HTMLElement | undefined} targetCell
     * @param {GridColumn} draggedColumn
     * @return {GridColumn | undefined}
     * @protected
     */
    _getTargetColumn(targetCell, draggedColumn) {
      if (targetCell && draggedColumn) {
        let candidate = targetCell._column;
        while (candidate.parentElement !== draggedColumn.parentElement && candidate !== this) {
          candidate = candidate.parentElement;
        }
        if (candidate.parentElement === draggedColumn.parentElement) {
          return candidate;
        }
        return targetCell._column;
      }
    }

    /**
     * Fired when the columns in the grid are reordered.
     *
     * @event column-reorder
     * @param {Object} detail
     * @param {Object} detail.columns the columns in the new order
     */
  };




© 2015 - 2024 Weber Informatics LLC | Privacy Policy