Multi-Range Selection

Set the FlexGrid's selectionMode property to MultiRange to enable Excel-style multi-range selection.

Users will be able to select multiple ranges by ctrl-clicking and dragging on the grid.

The selectedRanges property gets an array containing CellRange objects that contain the currently selected ranges.

The sample shows how you can provide Excel-style dynamic data summaries for the current selection (regular or multi-range).

It also shows how you can export selected ranges to CSV files.

Note that clipboard and export commands only work for multi-range selections if all selected ranges refer to the same column range or to the same row range. (Excel also behaves this way.)

import 'bootstrap.css'; import '@grapecity/wijmo.styles/wijmo.css'; import './styles.css'; import { glbz, isNumber } from '@grapecity/wijmo'; import { FlexGrid, SelectionMode, CellRange } from '@grapecity/wijmo.grid'; document.readyState === 'complete' ? init() : window.onload = init; function init() { // create some random data var data = []; var countries = 'Austria,Belgium,Chile,Denmark,Finland,Japan,UK'.split(','); for (var i = 0; i < 300; i++) { data.push({ id: i, from: countries[i % countries.length], to: countries[(i + 1) % countries.length], sales: Math.random() * 10000, expenses: Math.random() * 5000, amount: Math.random() * 10000, extra: Math.random() * 10000, }); } // show the data in a grid var theGrid = new FlexGrid('#theGrid', { alternatingRowStep: 0, showMarquee: true, anchorCursor: true, selectionMode: SelectionMode.MultiRange, itemsSource: data, // update aggregate display when selection changes selectionChanged: (s, e) => { // calculate aggregates let agg = { cnt: 0, cntAll: 0, sum: 0, avg: 0, cells: {} }; s.selectedRanges.forEach(rng => { aggregateRange(s, agg, rng); }); // update the display using template literals let msg = (agg.cnt > 1) ? glbz `Count: <b>${agg.cntAll}:n0</b>\tAverage: <b>${agg.avg}:g4\tSum: <b>${agg.sum}:g4</b>` : (agg.cntAll > 1) ? glbz `Count: <b>${agg.cntAll}:n0</b>` : 'Ready'; // update the display using wijmo.format //let msg = (agg.cnt > 1) ? format('Count: <b>{cntAll:n0}</b>\tAverage: <b>{avg:g4}</b>\tSum: <b>{sum:g4}</b>', agg) // : (agg.cntAll > 1) ? format('Count: <b>{cntAll:n0}</b>', agg) // : 'Ready'; document.getElementById('mr-aggregates').innerHTML = msg; } }); // update aggregates for a range function aggregateRange(grid, agg, rng) { for (let r = rng.topRow; r <= rng.bottomRow; r++) { for (let c = rng.leftCol; c <= rng.rightCol; c++) { let key = r + ',' + c; if (!agg.cells[key]) { // account for overlapping ranges agg.cells[key] = true; let data = grid.getCellData(r, c, false); if (isNumber(data)) { // handle numbers agg.cnt++; agg.sum += data; } if (data != '' && data != null) { // handle non-empty cells agg.cntAll++; } } } } agg.avg = agg.cnt > 0 ? agg.sum / agg.cnt : 0; } // export grid or selection to CSV document.getElementById('btn-csv-grid').addEventListener('click', () => { var rng = new CellRange(0, 0, theGrid.rows.length - 1, theGrid.columns.length - 1), csv = theGrid.getClipString(rng, true, true); exportCSV(csv, 'FlexGrid.csv'); }); document.getElementById('btn-csv-sel').addEventListener('click', () => { var csv = theGrid.getClipString(null, true, true); exportCSV(csv, 'FlexGridSelection.csv'); }); function exportCSV(csv, fileName) { let fileType = 'txt/csv;charset=utf-8'; if (navigator.msSaveBlob) { // IE navigator.msSaveBlob(new Blob([csv], { type: fileType }), fileName); } else { let e = document.createElement('a'); e.setAttribute('href', 'data:' + fileType + ',' + encodeURIComponent(csv)); e.setAttribute('download', fileName); e.style.display = 'none'; document.body.appendChild(e); e.click(); document.body.removeChild(e); } } } <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>GrapeCity Wijmo FlexGrid Multiple Selection</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <div class="container-fluid"> <div id="theGrid"></div> <pre id="mr-aggregates">Ready</pre> <button id="btn-csv-grid" class="btn btn-primary"> Export Whole Grid </button> <button id="btn-csv-sel" class="btn btn-primary"> Export Selection </button> </div> </body> </html> .wj-flexgrid { height: 300px; } import 'bootstrap.css'; import '@grapecity/wijmo.styles/wijmo.css'; import './styles.css'; import { Component, enableProdMode, NgModule, ViewChild } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { BrowserModule } from '@angular/platform-browser'; import { WjGridModule } from '@grapecity/wijmo.angular2.grid'; import { glbz, format, isNumber } from '@grapecity/wijmo'; import { FlexGrid, CellRange } from '@grapecity/wijmo.grid'; @Component({ selector: 'app-component', templateUrl: 'src/app.component.html' }) export class AppComponent { data: any[]; // DataSvc will be passed by derived classes constructor() { this.data = this._getData(); } // reference to grid component @ViewChild('grid') grid: FlexGrid; // update aggregates when selection changes selectionChanged() { // calculate aggregates let agg = { cnt: 0, cntAll: 0, sum: 0, avg: 0, cells: {} }; this.grid.selectedRanges.forEach(rng => { this.aggregateRange(this.grid, agg, rng); }); // update the display using template literals let msg = (agg.cnt > 1) ? glbz`Count: <b>${agg.cntAll}:n0</b>\tAverage: <b>${agg.avg}:g4\tSum: <b>${agg.sum}:g4</b>` : (agg.cntAll > 1) ? glbz`Count: <b>${agg.cntAll}:n0</b>` : 'Ready' // update the display using wijmo.format //let msg = (agg.cnt > 1) ? format('Count: <b>{cntAll:n0}</b>\tAverage: <b>{avg:g4}</b>\tSum: <b>{sum:g4}</b>', agg) // : (agg.cntAll > 1) ? format('Count: <b>{cntAll:n0}</b>', agg) // : 'Ready'; document.getElementById('mr-aggregates').innerHTML = msg; } aggregateRange(grid: FlexGrid, agg: any, rng: CellRange) { for (let r = rng.topRow; r <= rng.bottomRow; r++) { for (let c = rng.leftCol; c <= rng.rightCol; c++) { let key = r + ',' + c; if (!agg.cells[key]) { // account for overlapping ranges agg.cells[key] = true; let data = grid.getCellData(r, c, false); if (isNumber(data)) { // handle numbers agg.cnt++; agg.sum += data; } if (data != '' && data != null) { // handle non-empty cells agg.cntAll++; } } } } agg.avg = agg.cnt > 0 ? agg.sum / agg.cnt : 0; } // export grid or selection to CSV exportGridToCsv(selection: boolean) { let rng = selection ? null // selection plus extended ranges : new CellRange(0, 0, this.grid.rows.length - 1, this.grid.columns.length - 1); let csv = this.grid.getClipString(rng, true, true); this.exportCSV(csv, selection ? 'FlexGridSelection.csv' : 'FlexGrid.csv'); } exportCSV(csv: string, fileName: string) { let fileType = 'txt/csv;charset=utf-8'; if (navigator.msSaveBlob) { // IE navigator.msSaveBlob(new Blob([csv], { type: fileType }), fileName); } else { let e = document.createElement('a'); e.setAttribute('href', 'data:' + fileType + ',' + encodeURIComponent(csv)); e.setAttribute('download', fileName); e.style.display = 'none'; document.body.appendChild(e); e.click(); document.body.removeChild(e); } } // create some random data private _getData() { let data = []; let countries = 'Austria,Belgium,Chile,Denmark,Finland,Japan,UK'.split(','); for (let i = 0; i < 300; i++) { data.push({ id: i, from: countries[i % countries.length], to: countries[(i+1) % countries.length], sales: Math.random() * 10000, expenses: Math.random() * 5000, amount: Math.random() * 10000, extra: Math.random() * 10000, }); } return data; } } @NgModule({ imports: [WjGridModule, BrowserModule], declarations: [AppComponent], bootstrap: [AppComponent] }) export class AppModule { } enableProdMode(); // Bootstrap application with hash style navigation and global services. platformBrowserDynamic().bootstrapModule(AppModule); <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>GrapeCity Wijmo FlexGrid Multiple Selection</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Polyfills --> <script src="node_modules/core-js/client/shim.min.js"></script> <script src="node_modules/zone.js/dist/zone.min.js"></script> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.js"></script> <script src="systemjs.config.js"></script> <script> // workaround to load 'rxjs/operators' from the rxjs bundle System.import('rxjs').then(function (m) { System.set(SystemJS.resolveSync('rxjs/operators'), System.newModule(m.operators)); System.import('./src/app.component'); }); </script> </head> <body> <app-component></app-component> </body> </html> <div class="container-fluid"> <wj-flex-grid #grid [alternatingRowStep]="0" [showMarquee]="true" [anchorCursor]="true" [selectionMode]="'MultiRange'" [itemsSource]="data" (selectionChanged)="selectionChanged()"> </wj-flex-grid> <pre id="mr-aggregates">Ready</pre> <button class="btn btn-primary" (click)="exportGridToCsv(false)"> Export Whole Grid </button> &nbsp; <button id="btn-csv-sel" class="btn btn-primary" (click)="exportGridToCsv(true)"> Export Selection </button> </div> .wj-flexgrid { max-height: 300px; } .extended-selection.wj-cell:not(.wj-header):not(.wj-group):not(.wj-state-selected):not(.wj-state-multi-selected) { color: white; background: #a47aff; } body { margin-bottom: 20px; } <template> <div class="container-fluid"> <!-- the grid --> <wj-flex-grid :alternatingRowStep="0" :showMarquee="true" :anchorCursor="true" :selectionMode="'MultiRange'" :itemsSource="data" :selectionChanged="selectionChanged" :initialized="initialized"> </wj-flex-grid> <pre id="mr-aggregates">Ready</pre> <button class="btn btn-primary" @click="exportGridToCsv(false)"> Export Whole Grid </button> <button id="btn-csv-sel" class="btn btn-primary" @click="exportGridToCsv(true)"> Export Selection </button> </div> </template> <script> import "@grapecity/wijmo.styles/wijmo.css"; import 'bootstrap.css'; import Vue from 'vue'; import '@grapecity/wijmo.vue2.grid'; import { glbz, format, isNumber } from '@grapecity/wijmo'; import { CellRange } from '@grapecity/wijmo.grid'; let App = Vue.extend({ name: 'app', data: function(){ return { data: this.getData(), grid: null } }, methods: { // create some random data getData: function() { let data = []; let countries = 'Austria,Belgium,Chile,Denmark,Finland,Japan,UK'.split(','); for (let i = 0; i < 300; i++) { data.push({ id: i, from: countries[i % countries.length], to: countries[(i+1) % countries.length], sales: Math.random() * 10000, expenses: Math.random() * 5000, amount: Math.random() * 10000, extra: Math.random() * 10000, }); } return data; }, initialized: function(s) { this.grid = s; }, // update aggregates when selection changes selectionChanged: function() { // calculate aggregates let agg = { cnt: 0, cntAll: 0, sum: 0, avg: 0, cells: {} }; this.grid.selectedRanges.forEach(rng => { this.aggregateRange(this.grid, agg, rng); }); // update the display using template literals let msg = (agg.cnt > 1) ? glbz`Count: <b>${agg.cntAll}:n0</b>\tAverage: <b>${agg.avg}:g4\tSum: <b>${agg.sum}:g4</b>` : (agg.cntAll > 1) ? glbz`Count: <b>${agg.cntAll}:n0</b>` : 'Ready' // update the display using wijmo.format //let msg = (agg.cnt > 1) ? format('Count: <b>{cntAll:n0}</b>\tAverage: <b>{avg:g4}</b>\tSum: <b>{sum:g4}</b>', agg) // : (agg.cntAll > 1) ? format('Count: <b>{cntAll:n0}</b>', agg) // : 'Ready'; document.getElementById('mr-aggregates').innerHTML = msg; }, aggregateRange: function(grid, agg, rng) { for (let r = rng.topRow; r <= rng.bottomRow; r++) { for (let c = rng.leftCol; c <= rng.rightCol; c++) { let key = r + ',' + c; if (!agg.cells[key]) { // account for overlapping ranges agg.cells[key] = true; let data = grid.getCellData(r, c, false); if (isNumber(data)) { // handle numbers agg.cnt++; agg.sum += data; } if (data != '' && data != null) { // handle non-empty cells agg.cntAll++; } } } } agg.avg = agg.cnt > 0 ? agg.sum / agg.cnt : 0; }, // export grid or selection to CSV exportGridToCsv: function(selection) { let rng = selection ? null // selection plus extended selection : new CellRange(0, 0, this.grid.rows.length - 1, this.grid.columns.length - 1); let csv = this.grid.getClipString(rng, true, true); this.exportCSV(csv, selection ? 'FlexGridSelection.csv' : 'FlexGrid.csv'); }, exportCSV: function(csv, fileName) { let fileType = 'txt/csv;charset=utf-8'; if (navigator.msSaveBlob) { // IE navigator.msSaveBlob(new Blob([csv], { type: fileType }), fileName); } else { let e = document.createElement('a'); e.setAttribute('href', 'data:' + fileType + ',' + encodeURIComponent(csv)); e.setAttribute('download', fileName); e.style.display = 'none'; document.body.appendChild(e); e.click(); document.body.removeChild(e); } } } }) new Vue({ render: h => h(App) }).$mount('#app'); </script> <style> .wj-flexgrid { height: 300px; } </style> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>GrapeCity Wijmo FlexGrid Multiple Selection</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app.vue'); </script> </head> <body> <div id="app"> </div> </body> </html> import "@grapecity/wijmo.styles/wijmo.css"; import "bootstrap.css"; import "./app.css"; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { glbz, isNumber } from "@grapecity/wijmo"; import { FlexGrid } from "@grapecity/wijmo.react.grid"; import { CellRange } from "@grapecity/wijmo.grid"; class App extends React.Component { constructor(props) { super(props); this.grid = null; this.state = { data: this.getData() }; } render() { return <div className="container-fluid"> <FlexGrid alternatingRowStep={0} showMarquee={true} anchorCursor={true} selectionMode="MultiRange" itemsSource={this.state.data} initialized={s => this.grid = s} // update aggregate display when selection changes selectionChanged={(s) => { // calculate aggregates let agg = { cnt: 0, cntAll: 0, sum: 0, avg: 0, cells: {} }; s.selectedRanges.forEach(rng => { this.aggregateRange(s, agg, rng); }); // update the display using template literals let msg = (agg.cnt > 1) ? glbz `Count: <b>${agg.cntAll}:n0</b>\tAverage: <b>${agg.avg}:g4\tSum: <b>${agg.sum}:g4</b>` : (agg.cntAll > 1) ? glbz `Count: <b>${agg.cntAll}:n0</b>` : 'Ready'; // update the display using wijmo.format //let msg = (agg.cnt > 1) ? format('Count: <b>{cntAll:n0}</b>\tAverage: <b>{avg:g4}</b>\tSum: <b>{sum:g4}</b>', agg) // : (agg.cntAll > 1) ? format('Count: <b>{cntAll:n0}</b>', agg) // : 'Ready'; document.getElementById('mr-aggregates').innerHTML = msg; }}/> <pre id="mr-aggregates">Ready</pre> <button className="btn btn-primary" onClick={() => this.exportGridToCsv(this.grid, false)}> Export Whole Grid </button> {' '} <button id="btn-csv-sel" className="btn btn-primary" onClick={() => this.exportGridToCsv(this.grid, true)}> Export Selection </button> </div>; } // update aggregates for a range aggregateRange(grid, agg, rng) { for (let r = rng.topRow; r <= rng.bottomRow; r++) { for (let c = rng.leftCol; c <= rng.rightCol; c++) { let key = r + ',' + c; if (!agg.cells[key]) { // account for overlapping ranges agg.cells[key] = true; let data = grid.getCellData(r, c, false); if (isNumber(data)) { // handle numbers agg.cnt++; agg.sum += data; } if (data != '' && data != null) { // handle non-empty cells agg.cntAll++; } } } } agg.avg = agg.cnt > 0 ? agg.sum / agg.cnt : 0; } // export the grid or selection to CSV exportGridToCsv(grid, selection) { let rng = selection ? null // selection plus extended selection : new CellRange(0, 0, grid.rows.length - 1, grid.columns.length - 1); let csv = grid.getClipString(rng, true, true); this.exportCSV(csv, selection ? 'FlexGridSelection.csv' : 'FlexGrid.csv'); } exportCSV(csv, fileName) { let fileType = 'txt/csv;charset=utf-8'; if (navigator.msSaveBlob) { // IE navigator.msSaveBlob(new Blob([csv], { type: fileType }), fileName); } else { let e = document.createElement('a'); e.setAttribute('href', 'data:' + fileType + ',' + encodeURIComponent(csv)); e.setAttribute('download', fileName); e.style.display = 'none'; document.body.appendChild(e); e.click(); document.body.removeChild(e); } } // create some random data getData() { let data = []; let countries = 'Austria,Belgium,Chile,Denmark,Finland,Japan,UK'.split(','); for (let i = 0; i < 300; i++) { data.push({ id: i, from: countries[i % countries.length], to: countries[(i + 1) % countries.length], sales: Math.random() * 10000, expenses: Math.random() * 5000, amount: Math.random() * 10000, extra: Math.random() * 10000, }); } return data; } } ReactDOM.render(<App />, document.getElementById('app')); <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>GrapeCity Wijmo FlexGrid Multiple Selection</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <div id="app"></div> </body> </html> .wj-flexgrid { height: 300px; }