From 229ac6fb7821384e551d1552556b769b1caf2ceb Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Mon, 23 Dec 2024 12:29:35 +0400 Subject: [PATCH] docs: explain how to implement Grid range selection (#4026) * refactor: move Selection section to separate page * raise headings level * add introduction * add examples * polish * polish * polish * polish * polish * polish * fix examples * fix examples * align name of the method * polish text * fix typo * use map + sort to calculate range boundaries * remove unintentional changes * use selectionModel.selectItems instead of asMultiSelect.select * First pass at editing new text, only. * Second pass at editing new text. --------- Co-authored-by: Russell J.T. Dyer <6652767+russelljtdyer@users.noreply.github.com> --- articles/components/grid/selection.adoc | 33 +++++++- .../component/grid/grid-range-selection.ts | 80 +++++++++++++++++++ .../grid/react/grid-range-selection.tsx | 66 +++++++++++++++ .../component/grid/GridRangeSelection.java | 71 ++++++++++++++++ 4 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 frontend/demo/component/grid/grid-range-selection.ts create mode 100644 frontend/demo/component/grid/react/grid-range-selection.tsx create mode 100644 src/main/java/com/vaadin/demo/component/grid/GridRangeSelection.java diff --git a/articles/components/grid/selection.adoc b/articles/components/grid/selection.adoc index 417dea1689..63e8223c55 100644 --- a/articles/components/grid/selection.adoc +++ b/articles/components/grid/selection.adoc @@ -68,11 +68,40 @@ include::{root}/frontend/demo/component/grid/react/grid-multi-select-mode.tsx[re endif::[] -- -In addition to selecting rows individually, a range of rows can be selected by dragging from one selection checkbox to another, if enabled: +=== Range Selection + +In addition to selecting rows individually, you may want to allow users to select or deselect a range of rows using kbd:[Shift] + Click. Since there are a variety ways for range selection, Grid provides the necessary event, `item-toggle` to create your own implementation. This event provides information about the toggled row, its selection state, and whether the user was holding kbd:[Shift] during the toggle. + +The example below demonstrates a possible implementation of range selection using `item-toggle`. In this implementation, the first clicked row is stored as an anchor point. When the user holds kbd:[Shift] and clicks another row, the selection state of all rows between the anchor and the newly clicked row is updated to match the clicked row's state. The second clicked row then becomes the new anchor point for future selections. [.example] -- +ifdef::lit[] +[source,typescript] +---- +include::{root}/frontend/demo/component/grid/grid-range-selection.ts[render,tags=snippet,indent=0,group=Lit] +---- +endif::[] + +ifdef::flow[] +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/grid/GridRangeSelection.java[render,tags=snippet,indent=0,group=Flow] +---- +endif::[] +ifdef::react[] +[source,tsx] +---- +include::{root}/frontend/demo/component/grid/react/grid-range-selection.tsx[render,tags=snippet,indent=0,group=React] +---- +endif::[] +-- + +A range of rows can also be selected by dragging from one selection checkbox to another, if that's enabled: + +[.example] +-- [source,typescript] ---- @@ -97,9 +126,9 @@ selectionModel.setDragSelect(true); ... ---- - -- + == Selection Modes in Flow Each selection mode is represented by a [classname]`GridSelectionModel`, accessible through the [methodname]`getSelectionModel()` method, which can be cast that to the specific selection model type, [classname]`SingleSelectionModel` or [classname]`MultiSelectionModel`. These interfaces provide selection mode specific APIs for configuration and selection events. diff --git a/frontend/demo/component/grid/grid-range-selection.ts b/frontend/demo/component/grid/grid-range-selection.ts new file mode 100644 index 0000000000..b27113e315 --- /dev/null +++ b/frontend/demo/component/grid/grid-range-selection.ts @@ -0,0 +1,80 @@ +import 'Frontend/demo/init'; // hidden-source-line +import '@vaadin/grid'; +import '@vaadin/grid/vaadin-grid-selection-column.js'; +import { html, LitElement } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import type { GridItemToggleEvent } from '@vaadin/grid'; +import { getPeople } from 'Frontend/demo/domain/DataService'; +import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; +import { applyTheme } from 'Frontend/generated/theme'; + +// tag::snippet[] +@customElement('grid-range-selection') +export class Example extends LitElement { + protected override createRenderRoot() { + const root = super.createRenderRoot(); + // Apply custom theme (only supported if your app uses one) + applyTheme(root); + return root; + } + + @state() + private items: Person[] = []; + + @state() + private selectedItems: Person[] = []; + + private rangeStartItem?: Person; + + protected override async firstUpdated() { + const { people } = await getPeople(); + this.items = people; + } + + handleItemToggle(event: GridItemToggleEvent) { + const { item, selected, shiftKey } = event.detail; + + // If the anchor point isn't set, set it to the current item + this.rangeStartItem ??= item; + + if (shiftKey) { + // Calculcate the range of items between the anchor point and + // the current item + const [rangeStart, rangeEnd] = [this.rangeStartItem, item] + .map((i) => this.items.indexOf(i)) + .sort((a, b) => a - b); + const rangeItems = this.items.slice(rangeStart, rangeEnd + 1); + + // Update the selection state of items within the range + // based on the state of the current item + const newSelectedItems = new Set(this.selectedItems); + rangeItems.forEach((rangeItem) => { + if (selected) { + newSelectedItems.add(rangeItem); + } else { + newSelectedItems.delete(rangeItem); + } + }); + this.selectedItems = [...newSelectedItems]; + } + + // Update the anchor point to the current item + this.rangeStartItem = item; + } + + protected override render() { + return html` + + + + + + + `; + } +} +// end::snippet[] diff --git a/frontend/demo/component/grid/react/grid-range-selection.tsx b/frontend/demo/component/grid/react/grid-range-selection.tsx new file mode 100644 index 0000000000..905dc0c769 --- /dev/null +++ b/frontend/demo/component/grid/react/grid-range-selection.tsx @@ -0,0 +1,66 @@ +import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line +import React, { useCallback, useEffect, useRef } from 'react'; +import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line +import { useSignal } from '@vaadin/hilla-react-signals'; +import { Grid, GridItemToggleEvent } from '@vaadin/react-components/Grid.js'; +import { GridColumn } from '@vaadin/react-components/GridColumn.js'; +import { GridSelectionColumn } from '@vaadin/react-components/GridSelectionColumn.js'; +import { getPeople } from 'Frontend/demo/domain/DataService'; +import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; + +function Example() { + useSignals(); // hidden-source-line + const items = useSignal([]); + const selectedItems = useSignal([]); + const rangeStartItem = useRef(); + + useEffect(() => { + getPeople().then(({ people }) => { + items.value = people; + }); + }, []); + + const handleItemToggle = (event: GridItemToggleEvent) => { + const { item, selected, shiftKey } = event.detail; + + // If the anchor point isn't set, set it to the current item + rangeStartItem.current ??= item; + + if (shiftKey) { + // Calculcate the range of items between the anchor point and + // the current item + const [rangeStart, rangeEnd] = [rangeStartItem.current, item] + .map((i) => items.value.indexOf(i)) + .sort((a, b) => a - b); + const rangeItems = items.value.slice(rangeStart, rangeEnd + 1); + + // Update the selection state of items within the range + // based on the state of the current item + const newSelectedItems = new Set(selectedItems.value); + rangeItems.forEach((rangeItem) => { + if (selected) { + newSelectedItems.add(rangeItem); + } else { + newSelectedItems.delete(rangeItem); + } + }); + selectedItems.value = [...newSelectedItems]; + } + + // Update the anchor point to the current item + rangeStartItem.current = item; + }; + + return ( + // tag::snippet[] + + + + + + + // end::snippet[] + ); +} + +export default reactExample(Example); // hidden-source-line diff --git a/src/main/java/com/vaadin/demo/component/grid/GridRangeSelection.java b/src/main/java/com/vaadin/demo/component/grid/GridRangeSelection.java new file mode 100644 index 0000000000..b0295da8c2 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/grid/GridRangeSelection.java @@ -0,0 +1,71 @@ +package com.vaadin.demo.component.grid; + +import java.util.List; + +import com.vaadin.demo.domain.Person; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridMultiSelectionModel; +import com.vaadin.flow.component.grid.dataview.GridListDataView; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; +import com.vaadin.demo.DemoExporter; // hidden-source-line +import com.vaadin.demo.domain.DataService; + +@Route("grid-range-selection") +public class GridRangeSelection extends Div { + + private Person rangeStartItem; + + public GridRangeSelection() { + // tag::snippet[] + Grid grid = new Grid<>(Person.class, false); + grid.addColumn(Person::getFirstName).setHeader("First name"); + grid.addColumn(Person::getLastName).setHeader("Last name"); + grid.addColumn(Person::getEmail).setHeader("Email"); + + List people = DataService.getPeople(); + grid.setItems(people); + + GridMultiSelectionModel selectionModel = (GridMultiSelectionModel) grid + .setSelectionMode(Grid.SelectionMode.MULTI); + + selectionModel.addClientItemToggleListener(event -> { + Person item = event.getItem(); + + // If the anchor point isn't set, set it to the current item + if (rangeStartItem == null) { + rangeStartItem = item; + } + + if (event.isShiftKey()) { + // Calculcate the range of items between the anchor + // point and the current item + GridListDataView dataView = grid.getListDataView(); + int rangeStart = dataView.getItemIndex(rangeStartItem).get(); + int rangeEnd = dataView.getItemIndex(item).get(); + Person[] rangeItems = dataView.getItems() + .skip(Math.min(rangeStart, rangeEnd)) + .limit(Math.abs(rangeStart - rangeEnd) + 1) + .toArray(Person[]::new); + + // Update the selection state of items within the range + // based on the state of the current item + if (event.isSelected()) { + selectionModel.selectItems(rangeItems); + } else { + selectionModel.deselectItems(rangeItems); + } + } + + // Update the anchor point to the current item + rangeStartItem = item; + }); + // end::snippet[] + + add(grid); + } + + public static class Exporter // hidden-source-line + extends DemoExporter { // hidden-source-line + } // hidden-source-line +}