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

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

Go to download

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

The 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 { addListener } from '@vaadin/component-base/src/gestures.js';

/**
 * A mixin that provides basic functionality for the
 * ``. This includes properties, cell rendering,
 * and overridable methods for handling changes to the selection state.
 *
 * **NOTE**: This mixin is re-used by the Flow component, and as such must not
 * implement any selection state updates for the column element or the grid.
 * Web component-specific selection state updates must be implemented in the
 * `` itself, by overriding the protected methods
 * provided by this mixin.
 *
 * @polymerMixin
 */
export const GridSelectionColumnBaseMixin = (superClass) =>
  class GridSelectionColumnBaseMixin extends superClass {
    static get properties() {
      return {
        /**
         * Width of the cells for this column.
         */
        width: {
          type: String,
          value: '58px',
          sync: true,
        },

        /**
         * Override `autoWidth` to enable auto-width
         */
        autoWidth: {
          type: Boolean,
          value: true,
        },

        /**
         * Flex grow ratio for the cell widths. When set to 0, cell width is fixed.
         * @attr {number} flex-grow
         * @type {number}
         */
        flexGrow: {
          type: Number,
          value: 0,
          sync: true,
        },

        /**
         * When true, all the items are selected.
         * @attr {boolean} select-all
         * @type {boolean}
         */
        selectAll: {
          type: Boolean,
          value: false,
          notify: true,
          sync: true,
        },

        /**
         * When true, the active gets automatically selected.
         * @attr {boolean} auto-select
         * @type {boolean}
         */
        autoSelect: {
          type: Boolean,
          value: false,
          sync: true,
        },

        /**
         * When true, rows can be selected by dragging over the selection column.
         * @attr {boolean} drag-select
         * @type {boolean}
         */
        dragSelect: {
          type: Boolean,
          value: false,
          sync: true,
        },

        /** @protected */
        _indeterminate: {
          type: Boolean,
          sync: true,
        },

        /** @protected */
        _selectAllHidden: Boolean,
      };
    }

    static get observers() {
      return [
        '_onHeaderRendererOrBindingChanged(_headerRenderer, _headerCell, path, header, selectAll, _indeterminate, _selectAllHidden)',
      ];
    }

    /**
     * Renders the Select All checkbox to the header cell.
     *
     * @override
     */
    _defaultHeaderRenderer(root, _column) {
      let checkbox = root.firstElementChild;
      if (!checkbox) {
        checkbox = document.createElement('vaadin-checkbox');
        checkbox.setAttribute('aria-label', 'Select All');
        checkbox.classList.add('vaadin-grid-select-all-checkbox');
        root.appendChild(checkbox);
        // Add listener after appending, so we can skip the initial change event
        checkbox.addEventListener('checked-changed', this.__onSelectAllCheckedChanged.bind(this));
      }

      const checked = this.__isChecked(this.selectAll, this._indeterminate);
      checkbox.__rendererChecked = checked;
      checkbox.checked = checked;
      checkbox.hidden = this._selectAllHidden;
      checkbox.indeterminate = this._indeterminate;
    }

    /**
     * Renders the Select Row checkbox to the body cell.
     *
     * @override
     */
    _defaultRenderer(root, _column, { item, selected }) {
      let checkbox = root.firstElementChild;
      if (!checkbox) {
        checkbox = document.createElement('vaadin-checkbox');
        checkbox.setAttribute('aria-label', 'Select Row');
        root.appendChild(checkbox);
        // Add listener after appending, so we can skip the initial change event
        checkbox.addEventListener('checked-changed', this.__onSelectRowCheckedChanged.bind(this));
        addListener(root, 'track', this.__onCellTrack.bind(this));
        root.addEventListener('mousedown', this.__onCellMouseDown.bind(this));
        root.addEventListener('click', this.__onCellClick.bind(this));
      }

      checkbox.__item = item;
      checkbox.__rendererChecked = selected;
      checkbox.checked = selected;
    }

    /**
     * Updates the select all state when the Select All checkbox is switched.
     * The listener handles only user-fired events.
     *
     * @private
     */
    __onSelectAllCheckedChanged(e) {
      // Skip if the state is changed by the renderer.
      if (e.target.checked === e.target.__rendererChecked) {
        return;
      }

      if (this._indeterminate || e.target.checked) {
        this._selectAll();
      } else {
        this._deselectAll();
      }
    }

    /**
     * Selects or deselects the row when the Select Row checkbox is switched.
     * The listener handles only user-fired events.
     *
     * @private
     */
    __onSelectRowCheckedChanged(e) {
      // Skip if the state is changed by the renderer.
      if (e.target.checked === e.target.__rendererChecked) {
        return;
      }

      if (e.target.checked) {
        this._selectItem(e.target.__item);
      } else {
        this._deselectItem(e.target.__item);
      }
    }

    /** @private */
    __onCellTrack(event) {
      if (!this.dragSelect) {
        return;
      }
      this.__dragCurrentY = event.detail.y;
      this.__dragDy = event.detail.dy;
      if (event.detail.state === 'start') {
        const renderedRows = this._grid._getRenderedRows();
        // Get the row where the drag started
        const dragStartRow = renderedRows.find((row) => row.contains(event.currentTarget.assignedSlot));
        // Whether to select or deselect the items on drag
        this.__selectOnDrag = !this._grid._isSelected(dragStartRow._item);
        // Store the index of the row where the drag started
        this.__dragStartIndex = dragStartRow.index;
        // Store the item of the row where the drag started
        this.__dragStartItem = dragStartRow._item;
        // Start the auto scroller
        this.__dragAutoScroller();
      } else if (event.detail.state === 'end') {
        // if drag start and end stays within the same item, then toggle its state
        if (this.__dragStartItem) {
          if (this.__selectOnDrag) {
            this._selectItem(this.__dragStartItem);
          } else {
            this._deselectItem(this.__dragStartItem);
          }
        }
        // clear drag state after timeout, which allows preventing the
        // subsequent click event if drag started and ended on the same item
        setTimeout(() => {
          this.__dragStartIndex = undefined;
        });
      }
    }

    /** @private */
    __onCellMouseDown(e) {
      if (this.dragSelect) {
        // Prevent text selection when starting to drag
        e.preventDefault();
      }
    }

    /** @private */
    __onCellClick(e) {
      if (this.__dragStartIndex !== undefined) {
        // Stop the click event if drag was enabled. This click event should
        // only occur if drag started and stopped on the same item. In that case
        // the selection state has already been toggled on drag end, and we
        // don't  want to toggle it again from clicking the checkbox or changing
        // the active item.
        e.preventDefault();
      }
    }

    /** @private */
    _onCellKeyDown(e) {
      const target = e.composedPath()[0];
      // Toggle on Space without having to enter interaction mode first
      if (e.keyCode === 32 && (target === this._headerCell || (this._cells.includes(target) && !this.autoSelect))) {
        const checkbox = target._content.firstElementChild;
        checkbox.checked = !checkbox.checked;
      }
    }

    /** @private */
    __dragAutoScroller() {
      if (this.__dragStartIndex === undefined) {
        return;
      }
      // Get the row being hovered over
      const renderedRows = this._grid._getRenderedRows();
      const hoveredRow = renderedRows.find((row) => {
        const rowRect = row.getBoundingClientRect();
        return this.__dragCurrentY >= rowRect.top && this.__dragCurrentY <= rowRect.bottom;
      });

      // Get the index of the row being hovered over or the first/last
      // visible row if hovering outside the grid
      let hoveredIndex = hoveredRow ? hoveredRow.index : undefined;
      const scrollableArea = this.__getScrollableArea();
      if (this.__dragCurrentY < scrollableArea.top) {
        hoveredIndex = this._grid._firstVisibleIndex;
      } else if (this.__dragCurrentY > scrollableArea.bottom) {
        hoveredIndex = this._grid._lastVisibleIndex;
      }

      if (hoveredIndex !== undefined) {
        // Select all items between the start and the current row
        renderedRows.forEach((row) => {
          if (
            (hoveredIndex > this.__dragStartIndex && row.index >= this.__dragStartIndex && row.index <= hoveredIndex) ||
            (hoveredIndex < this.__dragStartIndex && row.index <= this.__dragStartIndex && row.index >= hoveredIndex)
          ) {
            if (this.__selectOnDrag) {
              this._selectItem(row._item);
            } else {
              this._deselectItem(row._item);
            }
            this.__dragStartItem = undefined;
          }
        });
      }

      // Start scrolling in the top/bottom 15% of the scrollable area
      const scrollTriggerArea = scrollableArea.height * 0.15;
      // Maximum number of pixels to scroll per iteration
      const maxScrollAmount = 10;

      if (this.__dragDy < 0 && this.__dragCurrentY < scrollableArea.top + scrollTriggerArea) {
        const dy = scrollableArea.top + scrollTriggerArea - this.__dragCurrentY;
        const percentage = Math.min(1, dy / scrollTriggerArea);
        this._grid.$.table.scrollTop -= percentage * maxScrollAmount;
      }
      if (this.__dragDy > 0 && this.__dragCurrentY > scrollableArea.bottom - scrollTriggerArea) {
        const dy = this.__dragCurrentY - (scrollableArea.bottom - scrollTriggerArea);
        const percentage = Math.min(1, dy / scrollTriggerArea);
        this._grid.$.table.scrollTop += percentage * maxScrollAmount;
      }

      // Schedule the next auto scroll
      setTimeout(() => this.__dragAutoScroller(), 10);
    }

    /**
     * Gets the scrollable area of the grid as a bounding client rect. The
     * scrollable area is the bounding rect of the grid minus the header and
     * footer.
     *
     * @private
     */
    __getScrollableArea() {
      const gridRect = this._grid.$.table.getBoundingClientRect();
      const headerRect = this._grid.$.header.getBoundingClientRect();
      const footerRect = this._grid.$.footer.getBoundingClientRect();

      return {
        top: gridRect.top + headerRect.height,
        bottom: gridRect.bottom - footerRect.height,
        left: gridRect.left,
        right: gridRect.right,
        height: gridRect.height - headerRect.height - footerRect.height,
        width: gridRect.width,
      };
    }

    /**
     * Override to handle the user selecting all items.
     * @protected
     */
    _selectAll() {}

    /**
     * Override to handle the user deselecting all items.
     * @protected
     */
    _deselectAll() {}

    /**
     * Override to handle the user selecting an item.
     * @param {Object} item the item to select
     * @protected
     */
    _selectItem(item) {}

    /**
     * Override to handle the user deselecting an item.
     * @param {Object} item the item to deselect
     * @protected
     */
    _deselectItem(item) {}

    /**
     * IOS needs indeterminate + checked at the same time
     * @private
     */
    __isChecked(selectAll, indeterminate) {
      return indeterminate || selectAll;
    }
  };




© 2015 - 2024 Weber Informatics LLC | Privacy Policy