Skip to content

Commit

Permalink
feat: add drag selection to grid (CP: #6243)
Browse files Browse the repository at this point in the history
* feat: add grid rows selection by dragging support

* refactor: rename attribute column-resizing to disable-text-selection

* test: add scrolling logic tests

* refactor: use _selectItem and _deselectItem functions

* improve scrolling tests

* bind event listeners in mixin

* revert column-resizing renaming

* remove lasso terminology

* remove unnecessary click handler

* cleanup

* rename API to dragSelect

* cleanup tests

* prevent toggling checkbox or active item after dragging on single cell

* add TS definition

* address review comments

---------

Co-authored-by: Tomi Virkki <virkki@vaadin.com>
Co-authored-by: Felipe Lang <felipe.lang@ext.vaadin.com>
  • Loading branch information
3 people authored and yuriy-fix committed Sep 26, 2023
1 parent a83018c commit de07fe4
Show file tree
Hide file tree
Showing 4 changed files with 385 additions and 2 deletions.
2 changes: 1 addition & 1 deletion dev/grid.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
</script>

<vaadin-grid item-id-path="name">
<vaadin-grid-selection-column auto-select frozen></vaadin-grid-selection-column>
<vaadin-grid-selection-column auto-select frozen drag-select></vaadin-grid-selection-column>
<vaadin-grid-tree-column frozen path="name" width="200px" flex-shrink="0"></vaadin-grid-tree-column>
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>
<vaadin-grid-column path="name" width="200px" flex-shrink="0"></vaadin-grid-column>
Expand Down
6 changes: 6 additions & 0 deletions packages/grid/src/vaadin-grid-selection-column.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ declare class GridSelectionColumn<TItem = GridDefaultItem> extends GridColumn<TI
*/
autoSelect: boolean;

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

addEventListener<K extends keyof GridSelectionColumnEventMap>(
type: K,
listener: (this: GridSelectionColumn<TItem>, ev: GridSelectionColumnEventMap[K]) => void,
Expand Down
151 changes: 151 additions & 0 deletions packages/grid/src/vaadin-grid-selection-column.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import '@vaadin/checkbox/src/vaadin-checkbox.js';
import { addListener } from '@vaadin/component-base/src/gestures.js';
import { GridColumn } from './vaadin-grid-column.js';

/**
Expand Down Expand Up @@ -76,6 +77,16 @@ class GridSelectionColumn extends GridColumn {
value: false,
},

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

/** @private */
__indeterminate: Boolean,

Expand Down Expand Up @@ -140,6 +151,9 @@ class GridSelectionColumn extends GridColumn {
checkbox.classList.add('vaadin-grid-select-all-checkbox');
checkbox.addEventListener('checked-changed', this.__onSelectAllCheckedChanged.bind(this));
root.appendChild(checkbox);
addListener(root, 'track', this.__onCellTrack.bind(this));
root.addEventListener('mousedown', this.__onCellMouseDown.bind(this));
root.addEventListener('click', this.__onCellClick.bind(this));
}

const checked = this.__isChecked(this.selectAll, this.__indeterminate);
Expand Down Expand Up @@ -236,6 +250,143 @@ class GridSelectionColumn extends GridColumn {
}
}

/** @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.__dragSelect = !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.__dragSelect) {
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 */
__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.__dragSelect) {
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,
};
}

/**
* IOS needs indeterminate + checked at the same time
* @private
Expand Down

0 comments on commit de07fe4

Please sign in to comment.