From 3c5a0b5a539d9b40ce52f5d9c4060ca03b5a1712 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Thu, 19 Dec 2024 15:25:59 +0400 Subject: [PATCH] add examples --- articles/components/grid/selection.adoc | 27 +++++-- .../component/grid/grid-range-selection.ts | 79 +++++++++++++++++++ .../grid/react/grid-range-selection.tsx | 68 ++++++++++++++++ .../component/grid/GridRangeSelection.java | 68 ++++++++++++++++ 4 files changed, 237 insertions(+), 5 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 c62afc5f4c..2756744d90 100644 --- a/articles/components/grid/selection.adoc +++ b/articles/components/grid/selection.adoc @@ -70,14 +70,33 @@ endif::[] === Range Selection -In addition to selecting rows individually, you may want to enable users to select a range of rows by holding kbd:[Shift]. Since there is a variety of possible behaviors for range selection, Grid provides the `item-toggle` event to create your own implemention. Typically, you would use this event to track the first and last selected rows. When kbd:[Shift] is pressed, you can then select or deselect all rows between them based on their selection state. +In addition to selecting rows individually, you may want to enable users to select or deselect a range of rows using kbd:[Shift] + Click. Since there is a variety of possible behaviors for range selection, Grid provides the necessary event, `item-toggle`, to create your own implemention. This event provides information about the toggled row, its selection state, and whether the user was holding kbd:[Shift] during the toggle. -In the example below, the first toggled row is stored in the `firstItem` variable. When the user holds kbd:[Shift] and selects a row, all rows between the first and current row are selected. Similarly, if the user deselected a row, all rows within the range are also deselected. After that, the `firstItem` variable is updated to the current row for the next selection. +The example below demonstrates a possible implementation of range selection using that event. In this implementation, the first toggled row is stored as an anchor point. When the user holds kbd:[Shift] and toggles another row, the selection state of all rows between the anchor and the target row is updated to match the target row's state. After that, the target row becomes the new anchor point. [.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;snippet2,indent=0,group=Flow] +---- +endif::[] -// TODO: Add example +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 enabled: @@ -112,8 +131,6 @@ selectionModel.setDragSelect(true); -- -[.example] - == 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..c059ba5a1a --- /dev/null +++ b/frontend/demo/component/grid/grid-range-selection.ts @@ -0,0 +1,79 @@ +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, query, state } from 'lit/decorators.js'; +import { getPeople } from 'Frontend/demo/domain/DataService'; +import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; +import { applyTheme } from 'Frontend/generated/theme'; +import { Grid } from '@vaadin/grid'; + +// 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; + } + + @query('vaadin-grid') + private grid!: Grid; + + @state() + private items: Person[] = []; + + @state() + private startItem!: Person; + + protected override async firstUpdated() { + const { people } = await getPeople(); + this.items = people; + } + + onItemToggle(event: GridItemToggleEvent) { + const { item, selected, shiftKey } = event.detail; + + // If the anchor point isn't set, set it to the current item + this.startItem ??= item; + + if (shiftKey) { + // Calculcate the range of items between the anchor point and + // the current item + const startIndex = this.items.indexOf(this.startItem); + const endIndex = this.items.indexOf(item); + const rangeItems = this.items.slice( + Math.min(startIndex, endIndex), + Math.max(startIndex, endIndex) + 1 + ); + + // Update the selection state of the items within the range + // based on the state of the current item + const selectedItems = new Set(this.grid.selectedItems); + rangeItems.forEach((rangeItem) => { + if (selected) { + selectedItems.add(rangeItem); + } else { + selectedItems.delete(rangeItem); + } + }); + this.grid.selectedItems = [...selectedItems]; + } + + // Update the anchor point to the current item + this.startItem = 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..29cabdbe2c --- /dev/null +++ b/frontend/demo/component/grid/react/grid-range-selection.tsx @@ -0,0 +1,68 @@ +import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line +import React, { useEffect, useRef } from 'react'; +import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line +import { useSignal } from '@vaadin/hilla-react-signals'; +import { Grid } 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 startItem = useRef(); + + useEffect(() => { + getPeople().then(({ people }) => { + items.value = people; + }); + }, []); + + const handleItemToggle = (event: CustomEvent) => { + const { item, selected, shiftKey } = event.detail; + + // If the anchor point isn't set, set it to the current item + startItem.current ??= item; + + if (shiftKey) { + // Calculcate the range of items between the anchor point and + // the current item + const startIndex = items.value.indexOf(startItem.current!); + const endIndex = items.value.indexOf(item); + const rangeItems = items.value.slice( + Math.min(startIndex, endIndex), + Math.max(startIndex, endIndex) + 1 + ); + + // Update the selection state of the items within the range + // based on the state of the current item + const selectedItemsCopy = new Set(selectedItems.value); + rangeItems.forEach((rangeItem) => { + if (selected) { + selectedItemsCopy.add(rangeItem); + } else { + selectedItemsCopy.delete(rangeItem); + } + }); + selectedItems.value = [...selectedItemsCopy]; + } + + // Update the anchor point to the current item + startItem.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..4417bda834 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/grid/GridRangeSelection.java @@ -0,0 +1,68 @@ +package com.vaadin.demo.component.grid; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +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 startItem; + + public GridRangeSelection() { + 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); + + // tag::snippet[] + grid.setSelectionMode(Grid.SelectionMode.MULTI); + ((GridMultiSelectionModel) grid.getSelectionModel()) + .addClientItemToggleListener(event -> { + Person item = event.getItem(); + + startItem = startItem != null ? startItem : item; + if (event.isShiftKey()) { + Set range = fetchItemsRange(grid, startItem, + item); + if (event.isSelected()) { + grid.asMultiSelect().select(range); + } else { + grid.asMultiSelect().deselect(range); + } + } + startItem = item; + }); + // end::snippet[] + + add(grid); + } + + // tag::snippet2[] + private Set fetchItemsRange(Grid grid, T startItem, T endItem) { + GridListDataView dataView = grid.getListDataView(); + int startIndex = dataView.getItemIndex(startItem).get(); + int endIndex = dataView.getItemIndex(endItem).get(); + + return dataView.getItems().skip(Math.min(startIndex, endIndex)) + .limit(Math.abs(startIndex - endIndex) + 1) + .collect(Collectors.toSet()); + } + // end::snippet2[] + + public static class Exporter // hidden-source-line + extends DemoExporter { // hidden-source-line + } // hidden-source-line +}