Skip to content

Multi-Range Selection

MultiRangeSelectionPlugin adds spreadsheet-style disjoint range selection while leaving the native RevoGrid active range in charge of focus, Shift extension, and editing.

Use it when users need to compare, clear, or copy multiple non-contiguous regions from the same grid.

Source code
TypeScript ts
import { defineCustomElements } from '@revolist/revogrid/loader';
import {
  CellFlashPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  MultiRangeSelectionPlugin,
  RowOddPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import { columns, createRows, emptySelectionSummary, formatSelectedCells } from './data';

defineCustomElements();
const { isDark } = currentTheme();

export function load(parentSelector: string) {
  const rows = createRows();
  const container = document.createElement('div');
  container.className = 'grid gap-3';

  const grid = document.createElement('revo-grid') as HTMLRevoGridElement;
  grid.className = 'rounded-lg overflow-hidden';
  grid.range = true;
  grid.theme = isDark() ? 'darkMaterial' : 'material';
  grid.hideAttribution = true;
  grid.columns = columns;
  grid.stretch = 'all';
  grid.eventManager = {
    applyEventsToSource: true,
  };
  grid.plugins = [
    EventManagerPlugin,
    MultiRangeSelectionPlugin,
    CellFlashPlugin,
    RowOddPlugin,
    ColumnStretchPlugin,
  ];
  grid.style.minHeight = '420px';
  grid.style.minWidth = '620px';

  const panel = document.createElement('pre');
  panel.className = 'rounded-lg border border-slate-200 bg-slate-950 p-3 text-xs text-slate-100 overflow-auto';
  panel.style.minHeight = '120px';
  panel.textContent = emptySelectionSummary;

  const updateSummary = (event: Event) => {
    const detail = (event as CustomEvent).detail;
    panel.textContent = formatSelectedCells(detail?.ranges || [], rows);
  };
  grid.addEventListener('multirangeselectionchange', updateSummary);

  container.append(grid, panel);
  document.querySelector(parentSelector)?.appendChild(container);
  grid.source = rows;

  return () => {
    grid.removeEventListener('multirangeselectionchange', updateSummary);
    container.remove();
  };
}
Data ts
import type { ColumnRegular, DataType } from '@revolist/revogrid';
import { cellFlashArrowTemplate, type MultiRangeSelectionRange } from '@revolist/revogrid-pro';

export const columns: ColumnRegular[] = [
  { name: 'Region', prop: 'region', size: 130, flash: () => true },
  { name: 'Account', prop: 'account', size: 150, flash: () => true },
  { name: 'Status', prop: 'status', size: 120, flash: () => true },
  { name: 'Owner', prop: 'owner', size: 130, flash: () => true },
  {
    name: 'Amount',
    prop: 'amount',
    size: 110,
    flash: () => true,
    cellTemplate: cellFlashArrowTemplate(),
  },
];

export function createRows(): DataType[] {
  return [
    { region: 'North', account: 'Acme', status: 'Draft', owner: 'Mia', amount: 1280 },
    { region: 'North', account: 'Beacon', status: 'Approved', owner: 'Noah', amount: 2140 },
    { region: 'West', account: 'Cobalt', status: 'Review', owner: 'Ava', amount: 980 },
    { region: 'West', account: 'Delta', status: 'Approved', owner: 'Leo', amount: 3420 },
    { region: 'East', account: 'Evergreen', status: 'Draft', owner: 'Ivy', amount: 1650 },
    { region: 'East', account: 'Fabrikam', status: 'Review', owner: 'Owen', amount: 2870 },
    { region: 'South', account: 'Globex', status: 'Approved', owner: 'Eli', amount: 1960 },
    { region: 'South', account: 'Harbor', status: 'Draft', owner: 'Zoe', amount: 1210 },
  ];
}

export const emptySelectionSummary = [
  'Selected cells: 0',
  'Use Ctrl/Cmd+click to add a range.',
].join('\n');

export function formatSelectedCells(
  ranges: MultiRangeSelectionRange[] = [],
  rows: DataType[] = [],
) {
  const lines: string[] = [];
  let count = 0;
  const maxLines = 24;

  for (const selection of ranges) {
    for (let rowIndex = selection.range.y; rowIndex <= selection.range.y1; rowIndex++) {
      for (let colIndex = selection.range.x; colIndex <= selection.range.x1; colIndex++) {
        count++;
        if (lines.length >= maxLines) {
          continue;
        }

        const column = columns[colIndex];
        const prop = column?.prop;
        const value = prop ? rows[rowIndex]?.[prop] : undefined;
        lines.push(
          `R${rowIndex + 1} ${column?.name || `C${colIndex + 1}`} = ${formatCellValue(value)}`,
        );
      }
    }
  }

  if (!count) {
    return emptySelectionSummary;
  }

  const hidden = count - lines.length;
  return [
    `Selected cells: ${count}`,
    ...lines,
    ...(hidden > 0 ? [`+${hidden} more`] : []),
  ].join('\n');
}

function formatCellValue(value: unknown) {
  if (value === null || typeof value === 'undefined') {
    return '';
  }
  return String(value);
}
Vue vue
<template>
  <div class="grid gap-3">
    <VGrid
      class="rounded-lg overflow-hidden"
      range
      :theme="isDark ? 'darkMaterial' : 'material'"
      :columns="columns"
      :source="rows"
      :plugins="plugins"
      :event-manager="eventManager"
      stretch="all"
      hide-attribution
      style="min-height: 420px; min-width: 620px;"
      @multirangeselectionchange="onSelectionChange"
    />
    <pre class="rounded-lg border border-slate-200 bg-slate-950 p-3 text-xs text-slate-100 overflow-auto min-h-30">{{ selectionSummary }}</pre>
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue';
import { VGrid } from '@revolist/vue3-datagrid';
import {
  CellFlashPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  MultiRangeSelectionPlugin,
  RowOddPlugin,
} from '@revolist/revogrid-pro';
import { currentThemeVue } from '../composables/useRandomData';
import { columns, createRows, emptySelectionSummary, formatSelectedCells } from './data';

const { isDark } = currentThemeVue();
const plugins = [
  EventManagerPlugin,
  MultiRangeSelectionPlugin,
  CellFlashPlugin,
  RowOddPlugin,
  ColumnStretchPlugin,
];
const rows = ref(createRows());
const selectionSummary = ref(emptySelectionSummary);
const eventManager = computed(() => ({
  applyEventsToSource: true,
}));

function onSelectionChange(event: CustomEvent) {
  selectionSummary.value = formatSelectedCells(event.detail?.ranges || [], rows.value);
}
</script>
React tsx
import React, { useCallback, useMemo, useState } from 'react';
import { RevoGrid, type ColumnRegular, type DataType } from '@revolist/react-datagrid';
import {
  CellFlashPlugin,
  ColumnStretchPlugin,
  type EventManagerConfig,
  EventManagerPlugin,
  MultiRangeSelectionPlugin,
  RowOddPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import { columns as baseColumns, createRows, emptySelectionSummary, formatSelectedCells } from './data';

export default function MultiRangeSelection() {
  const { isDark } = currentTheme();
  const theme = useMemo(() => (isDark() ? 'darkMaterial' : 'material'), [isDark]);
  const columns = useMemo<ColumnRegular[]>(() => baseColumns, []);
  const plugins = useMemo(
    () => [
      EventManagerPlugin,
      MultiRangeSelectionPlugin,
      CellFlashPlugin,
      RowOddPlugin,
      ColumnStretchPlugin,
    ],
    [],
  );
  const eventManager = useMemo<EventManagerConfig>(
    () => ({
      applyEventsToSource: true,
    }),
    [],
  );
  const [rows] = useState<DataType[]>(createRows);
  const [selectionSummary, setSelectionSummary] = useState(emptySelectionSummary);
  const handleSelectionChange = useCallback((event: CustomEvent) => {
    setSelectionSummary(formatSelectedCells(event.detail?.ranges || [], rows));
  }, [rows]);

  return (
    <div className="grid gap-3">
      <RevoGrid
        className="rounded-lg overflow-hidden"
        range
        theme={theme}
        columns={columns}
        source={rows}
        plugins={plugins}
        eventManager={eventManager}
        stretch="all"
        hide-attribution
        onMultirangeselectionchange={handleSelectionChange as any}
        style={{ minHeight: 420, minWidth: 620 }}
      />
      <pre className="rounded-lg border border-slate-200 bg-slate-950 p-3 text-xs text-slate-100 overflow-auto min-h-30">
        {selectionSummary}
      </pre>
    </div>
  );
}
Angular ts
import { Component, NO_ERRORS_SCHEMA, ViewEncapsulation, type OnInit } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import type { DataType } from '@revolist/revogrid';
import {
  CellFlashPlugin,
  ColumnStretchPlugin,
  type EventManagerConfig,
  EventManagerPlugin,
  MultiRangeSelectionPlugin,
  RowOddPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import { columns, createRows, emptySelectionSummary, formatSelectedCells } from './data';

@Component({
  selector: 'multi-range-selection-grid',
  standalone: true,
  imports: [RevoGrid],
  template: `
    <div class="grid gap-3">
      <revo-grid
        class="rounded-lg overflow-hidden"
        [range]="true"
        [theme]="theme"
        [columns]="columns"
        [source]="rows"
        [plugins]="plugins"
        [eventManager]="eventManager"
        [stretch]="'all'"
        [hideAttribution]="true"
        (multirangeselectionchange)="onSelectionChange($event)"
        style="min-height: 420px; min-width: 620px;"
      ></revo-grid>
      <pre class="rounded-lg border border-slate-200 bg-slate-950 p-3 text-xs text-slate-100 overflow-auto min-h-30">{{ selectionSummary }}</pre>
    </div>
  `,
  schemas: [NO_ERRORS_SCHEMA],
})
export class MultiRangeSelectionGridComponent implements OnInit {
  theme = currentTheme().isDark() ? 'darkMaterial' : 'material';
  columns = columns;
  plugins = [
    EventManagerPlugin,
    MultiRangeSelectionPlugin,
    CellFlashPlugin,
    RowOddPlugin,
    ColumnStretchPlugin,
  ];
  eventManager: EventManagerConfig = {
    applyEventsToSource: true,
  };
  rows: DataType[] = [];
  selectionSummary = emptySelectionSummary;

  ngOnInit() {
    this.rows = createRows();
  }

  onSelectionChange(event: Event) {
    const detail = (event as CustomEvent).detail;
    this.selectionSummary = formatSelectedCells(detail?.ranges || [], this.rows);
  }
}
import { MultiRangeSelectionPlugin } from '@revolist/revogrid-pro';
const grid = document.createElement('revo-grid');
grid.range = true;
grid.plugins = [MultiRangeSelectionPlugin];
  1. Select a cell or range normally.
  2. Ctrl/Cmd+click another cell to keep the previous range and start a new active range.
  3. Shift+click or Shift+Arrow extends only the active range.
  4. Tab and Enter move the focused cell inside the active range.
  5. Delete and Backspace clear inactive ranges plus the active range.
  6. Copy includes inactive ranges plus the active range.
  7. Plain click clears inactive ranges and starts a normal selection.

Multi-range copy preserves relative shape for nearby ranges in text/plain. For example, diagonal selected cells are copied with blank cells between them so external spreadsheets can keep the diagonal layout instead of stacking values one after another. RevoGrid also writes a custom multi-range clipboard payload; when the payload is pasted back into RevoGrid, only the selected cells are edited, so the blank gaps do not clear existing destination values. Very large sparse selections fall back to selected rectangular blocks to avoid huge clipboard payloads. Rich text/html output mirrors the selected clipboard shape.

If only one range is selected, RevoGrid keeps its default copy behavior.

The plugin adds convenience methods to the grid element while it is mounted:

grid.setMultiRangeSelectedRanges?.([
{ rowType: 'rgRow', colType: 'rgCol', range: { x: 0, y: 0, x1: 1, y1: 1 } },
{ rowType: 'rgRow', colType: 'rgCol', range: { x: 3, y: 0, x1: 3, y1: 1 } },
]);
const ranges = grid.getMultiRangeSelectedRanges?.();
grid.clearMultiRangeSelectedRanges?.();

You can also call the same operations on the plugin instance:

const plugin = (await grid.getPlugins())
.find(plugin => plugin instanceof MultiRangeSelectionPlugin);
const ranges = plugin?.getSelectedRanges();

The grid also dispatches multirangeselectionchange whenever the selected ranges change.

Inactive cells receive:

  • attribute: multi-range-selection
  • class: multi-range-selection-cell
  • edge classes for the range perimeter

The active range remains the native RevoGrid selection overlay.