Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add drag selection to grid (CP: #6243) #6543

Merged
merged 2 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -161,6 +172,9 @@ class GridSelectionColumn extends GridColumn {
checkbox.setAttribute('aria-label', 'Select Row');
checkbox.addEventListener('checked-changed', this.__onSelectRowCheckedChanged.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));
}

checkbox.__item = item;
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 visibleRows = this._grid._getVisibleRows();
// Get the row where the drag started
const dragStartRow = visibleRows.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._grid.selectItem(this.__dragStartItem);
} else {
this._grid.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 visibleRows = this._grid._getVisibleRows();
const hoveredRow = visibleRows.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
visibleRows.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._grid.selectItem(row._item);
} else {
this._grid.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
6 changes: 4 additions & 2 deletions packages/grid/test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,10 @@ export const getHeaderCellContent = (grid, row, col) => {
};

export const getBodyCellContent = (grid, row, col) => {
const container = grid.$.items;
return getContainerCellContent(container, row, col);
const physicalItems = getPhysicalItems(grid);
const physicalRow = physicalItems.find((item) => item.index === row);
const cells = getRowCells(physicalRow);
return getCellContent(cells[col]);
};

export const getContainerCellContent = (container, row, col) => {
Expand Down