Rows and Columns Context Menu
This example demonstrates how to create separate context menus with interactive actions for grid rows and columns. Use the same ContextMenuPlugin, but pass row actions through rowContextMenu and column-header actions through columnContextMenu.
Key Features:
- Separate row and column context menu configurations
- Actions with visual indicators
- Styled menu buttons with hover effects
Source code
import '@fortawesome/fontawesome-free/css/all.min.css';
import { defineCustomElements } from '@revolist/revogrid/loader';defineCustomElements();
import { currentTheme, useRandomData } from '../composables/useRandomData';import { ContextMenuPlugin, RowHeaderPlugin, ColumnStretchPlugin, RowOddPlugin,} from '@revolist/revogrid-pro';import { columnContextMenuConfig, rowContextMenuConfig,} from './context-menu.config';const { createRandomData } = useRandomData();const { isDark } = currentTheme();
export function load(parentSelector: string) { const parent = document.querySelector(parentSelector); if (!parent) { return; }
const grid = document.createElement('revo-grid'); grid.source = createRandomData(100); // Define columns grid.columns = [ { name: '#', prop: 'id', size: 70, pin: 'colPinStart', }, { name: '🍎 Fruit', prop: 'name', }, { name: '💰 Price', prop: 'price', pin: 'colPinEnd', }, ]; // Define plugin grid.plugins = [ RowHeaderPlugin, ContextMenuPlugin, ColumnStretchPlugin, RowOddPlugin, ]; // grid.rowHeaders = rowHeaders({ showHeaderFocusBtn: true });
Object.assign(grid, { // Define separate context menus for row and column targets rowContextMenu: rowContextMenuConfig, columnContextMenu: columnContextMenuConfig, stretch: 'all', });
// Set theme grid.theme = isDark() ? 'darkMaterial' : 'material'; grid.hideAttribution = true; parent.appendChild(grid);}<template> <RevoGrid class="rounded-lg overflow-hidden" :theme="isDark ? 'darkMaterial' : 'material'" :source="source" :columns="columns" :plugins="plugins" :row-context-menu.prop="rowContextMenuConfig" :column-context-menu.prop="columnContextMenuConfig" stretch="all" :rowHeaders="rowHeadersConfig" hideAttribution style="min-height: 300px" /></template>
<script setup lang="ts">import RevoGrid, { type ColumnRegular } from '@revolist/vue3-datagrid';import { ContextMenuPlugin, RowHeaderPlugin, ColumnStretchPlugin, RowOddPlugin, RowOrderPlugin, rowHeaders,} from '@revolist/revogrid-pro';import { ref } from 'vue';import { columnContextMenuConfig, rowContextMenuConfig,} from './context-menu.config';import { makeData } from '../composables/makeData';import { currentThemeVue } from '../composables/useRandomData';
const { isDark } = currentThemeVue();
const source = ref(makeData(100));
const columns: ColumnRegular[] = [ { name: '#', prop: 'id', size: 150, pin: 'colPinStart', }, { name: 'Name', prop: 'fullName', }, { name: 'Job Title', prop: 'jobTitle', pin: 'colPinEnd', },];
const plugins = [ RowHeaderPlugin, ContextMenuPlugin, ColumnStretchPlugin, RowOddPlugin, RowOrderPlugin,];
const rowHeadersConfig = ref( rowHeaders({ showHeaderFocusBtn: false, rowDrag: true }),);</script>
<style lang="css" src="@fortawesome/fontawesome-free/css/all.min.css" />
<style scoped>:deep(.rowHeaders) { revogr-data .rgCell { padding: 0 !important; }}
:deep(.row-header-holder) { button { width: 100%; border: 0; background: none;
&:hover { background-color: var(--sl-color-gray-6); } }}</style>import '@fortawesome/fontawesome-free/css/all.min.css';
import React, { useMemo } from 'react';import { RevoGrid, type ColumnRegular } from '@revolist/react-datagrid';import { ContextMenuPlugin, RowHeaderPlugin, ColumnStretchPlugin, RowOddPlugin,} from '@revolist/revogrid-pro';import { columnContextMenuConfig, rowContextMenuConfig,} from './context-menu.config';import { currentTheme, useRandomData } from '../composables/useRandomData';
const { isDark } = currentTheme();const { createRandomData } = useRandomData();
function ContextMenu() { const source = useMemo(() => createRandomData(100), []);
const columns: ColumnRegular[] = useMemo( () => [ { name: '#', prop: 'id', size: 70, pin: 'colPinStart', }, { name: '🍎 Fruit', prop: 'name', }, { name: '💰 Price', prop: 'price', pin: 'colPinEnd', }, ], [], );
const plugins = useMemo( () => [ RowHeaderPlugin, ContextMenuPlugin, ColumnStretchPlugin, RowOddPlugin, ] as any, [], );
const RevoGridComponent = RevoGrid as any;
return ( <RevoGridComponent theme={isDark() ? 'darkMaterial' : 'material'} source={source} columns={columns} plugins={plugins} rowContextMenu={rowContextMenuConfig} columnContextMenu={columnContextMenuConfig} stretch="all" hideAttribution style={{ minHeight: '300px' }} /> );}
export default ContextMenu;import '@fortawesome/fontawesome-free/css/all.min.css';
import { Component, ViewEncapsulation, NO_ERRORS_SCHEMA } from '@angular/core';import { defineCustomElements } from '@revolist/revogrid/loader';import { ContextMenuPlugin, RowHeaderPlugin, ColumnStretchPlugin, RowOddPlugin,} from '@revolist/revogrid-pro';import { columnContextMenuConfig, rowContextMenuConfig,} from './context-menu.config';import { makeData } from '../composables/makeData';import { currentTheme } from '../composables/useRandomData';
defineCustomElements();
@Component({ selector: 'context-menu-grid', standalone: true, imports: [], template: ` <revo-grid [source]="source" [columns]="columns" [plugins]="plugins" [theme]="theme" [rowContextMenu]="rowContextMenu" [columnContextMenu]="columnContextMenu" [stretch]="stretch" [hideAttribution]="true" range style="min-height: 300px;" ></revo-grid> `, encapsulation: ViewEncapsulation.None, // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs. schemas: [NO_ERRORS_SCHEMA],})export class ContextMenuGridComponent { source = makeData(100);
columns = [ { name: '#', prop: 'id', size: 150, pin: 'colPinStart', }, { name: 'Name', prop: 'fullName', }, { name: 'Job Title', prop: 'jobTitle', pin: 'colPinEnd', }, ];
plugins = [ RowHeaderPlugin, ContextMenuPlugin, ColumnStretchPlugin, RowOddPlugin, ];
theme = currentTheme().isDark() ? 'darkMaterial' : 'material';
rowContextMenu = rowContextMenuConfig;
columnContextMenu = columnContextMenuConfig;
stretch = 'all';}import type { ColumnContextMenuOpenContext, ContextMenuActionContext, ContextMenuConfig, ContextMenuItem,} from '@revolist/revogrid-pro';import type { ColumnData, ColumnGrouping, ColumnProp, ColumnRegular,} from '@revolist/revogrid';import copySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg?raw';import scissorsSvg from '@fortawesome/fontawesome-free/svgs/solid/scissors.svg?raw';import pasteSvg from '@fortawesome/fontawesome-free/svgs/solid/paste.svg?raw';import arrowUpSvg from '@fortawesome/fontawesome-free/svgs/solid/arrow-up.svg?raw';import arrowDownSvg from '@fortawesome/fontawesome-free/svgs/solid/arrow-down.svg?raw';import trashSvg from '@fortawesome/fontawesome-free/svgs/solid/trash.svg?raw';import sortAscSvg from '@fortawesome/fontawesome-free/svgs/solid/arrow-up-a-z.svg?raw';import sortDescSvg from '@fortawesome/fontawesome-free/svgs/solid/arrow-down-z-a.svg?raw';import thumbtackSvg from '@fortawesome/fontawesome-free/svgs/solid/thumbtack.svg?raw';import infoSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-info.svg?raw';import fingerprintSvg from '@fortawesome/fontawesome-free/svgs/solid/fingerprint.svg?raw';
// Buffer to store copied/cut row data.let rowBuffer: any = null;
function getColumnContext( context?: ContextMenuActionContext,): ColumnContextMenuOpenContext | undefined { return context?.menu?.target === 'column' ? context.menu : undefined;}
function getGrid(context?: ContextMenuActionContext) { return context?.revogrid;}
function updateColumn( context: ContextMenuActionContext | undefined, updater: (column: ColumnRegular) => ColumnRegular,) { const columnContext = getColumnContext(context); const grid = getGrid(context); if (!grid || !columnContext?.column) { return; }
const updatedColumns = updateColumnsByProp( grid.columns || [], columnContext.column.prop, updater, );
grid.columns = updatedColumns;}
function sortColumn( context: ContextMenuActionContext | undefined, order: 'asc' | 'desc',) { const columnContext = getColumnContext(context); const grid = getGrid(context); if (!grid || !columnContext?.column) { return; }
updateColumn(context, (column) => ({ ...column, sortable: true, }));
void grid.updateColumnSorting( { prop: columnContext.column.prop, cellCompare: columnContext.column.cellCompare, }, order, false, );}
function isColumnGrouping( column: ColumnGrouping | ColumnRegular,): column is ColumnGrouping { return Array.isArray((column as ColumnGrouping).children);}
function updateColumnsByProp( columns: ColumnData, prop: ColumnProp, updater: (column: ColumnRegular) => ColumnRegular,): ColumnData { return columns.map((column) => { if (isColumnGrouping(column)) { return { ...column, children: updateColumnsByProp(column.children, prop, updater), }; }
if (column.prop !== prop) { return column; }
return updater({ ...column }); });}
export const rowContextMenuConfig: ContextMenuConfig = { items: [ { icon: copySvg, name: 'Copy row', action: (_, cell, __, ____, context) => { const grid = getGrid(context); if (!cell || !grid) return; // todo: it's virtual index, we need to convert it to physical index, it's not the same as the source index rowBuffer = { ...grid.source[cell.y] }; }, }, { icon: scissorsSvg, name: 'Cut row', action: (_, cell, __, ____, context) => { const grid = getGrid(context); if (!cell || !grid) return; rowBuffer = { ...grid.source[cell.y] }; // todo: it's virtual index, we need to convert it to physical index, it's not the same as the source index grid.source.splice(cell.y, 1); grid.source = [...grid.source]; }, }, { icon: pasteSvg, name: 'Paste row', hidden: () => !rowBuffer, action: (_, cell, __, ____, context) => { const grid = getGrid(context); if (!cell || !rowBuffer || !grid) return; const newRow = { ...rowBuffer }; // todo: it's virtual index, we need to convert it to physical index // it's not the same as the source index grid.source.splice(cell.y + 1, 0, newRow); grid.source = [...grid.source]; }, }, { icon: arrowUpSvg, name: 'Add row above', action: (_, cell, __, ____, context) => { const grid = getGrid(context); if (!cell || !grid) { return; } // todo: it's virtual index, we need to convert it to physical index // it's not the same as the source index grid.source.splice(cell.y, 0, { id: 0, name: 'New row', price: 0, }); grid.source = [...grid.source]; }, }, { icon: arrowDownSvg, name: 'Add row below', action: (_, cell, __, ____, context) => { const grid = getGrid(context); if (!cell || !grid) { return; } // todo: it's virtual index, we need to convert it to physical index // it's not the same as the source index grid.source.splice(cell.y + 1, 0, { id: 0, name: 'New row', price: 0, }); grid.source = [...grid.source]; }, }, { icon: trashSvg, name: (focused, range) => { if (!focused) { return ''; } if (!range) { range = { x: 0, y: focused.y, x1: 0, y1: focused.y, }; } // todo: it's virtual index, we need to convert it to physical index // it's not the same as the source index const rows = range.y1 - range.y + 1; if (!range || rows < 2) { return 'Delete row'; } return `Delete ${rows} rows`; }, action: (_, focused, range, __, context) => { const grid = getGrid(context); if (!focused || !grid) { return; }
if (!range) { range = { x: 0, y: focused.y, x1: 0, y1: focused.y, }; }
const rows = range.y1 - range.y + 1; // todo: it's virtual index, we need to convert it to physical index // it's not the same as the source index grid.source.splice(range.y, rows); grid.source = [...grid.source]; }, }, ],};
const sortColumnItems: ContextMenuItem[] = [ { icon: sortAscSvg, name: 'Sort ascending', action: (_, __, ___, ____, context) => sortColumn(context, 'asc'), }, { icon: sortDescSvg, name: 'Sort descending', action: (_, __, ___, ____, context) => sortColumn(context, 'desc'), },];
export const columnContextMenuConfig: ContextMenuConfig = { resolve: (context) => { if (context.target !== 'column') { return; }
if (context.column?.prop === 'id') { return { items: idColumnItems, anchorToTarget: true, }; }
if (context.columnType === 'colPinStart') { return { items: pinnedStartColumnItems, anchorToTarget: true, }; }
if (context.columnType === 'colPinEnd') { return { items: pinnedEndColumnItems, anchorToTarget: true, }; } }, items: [ ...sortColumnItems, { icon: thumbtackSvg, name: 'Pin column left', action: (_, __, ___, ____, context) => updateColumn(context, (column) => ({ ...column, pin: 'colPinStart', })), }, { icon: thumbtackSvg, name: 'Pin column right', action: (_, __, ___, ____, context) => updateColumn(context, (column) => ({ ...column, pin: 'colPinEnd', })), }, { icon: infoSvg, name: 'Log column info', action: (_, __, ___, ____, context) => { const columnContext = getColumnContext(context); console.log('Column info', { column: columnContext?.column, columnIndex: columnContext?.columnIndex, columnType: columnContext?.columnType, }); }, }, ],};
const idColumnItems: ContextMenuItem[] = [ { icon: fingerprintSvg, name: 'ID column', action: (_, __, ___, ____, context) => { console.log('ID column context', getColumnContext(context)); }, }, ...sortColumnItems,];
const pinnedStartColumnItems: ContextMenuItem[] = [ ...sortColumnItems, { icon: thumbtackSvg, name: 'Unpin left column', action: (_, __, ___, ____, context) => updateColumn(context, (column) => { delete column.pin; return column; }), },];
const pinnedEndColumnItems: ContextMenuItem[] = [ ...sortColumnItems, { icon: thumbtackSvg, name: 'Unpin right column', action: (_, __, ___, ____, context) => updateColumn(context, (column) => { delete column.pin; return column; }), }, { icon: infoSvg, name: 'Log pinned column info', action: (_, __, ___, ____, context) => { const columnContext = getColumnContext(context); console.log('Pinned column info', { column: columnContext?.column, columnIndex: columnContext?.columnIndex, columnType: columnContext?.columnType, }); }, },];