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
TypeScript ts
// src/components/row-header/rowHeader.ts
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);
}
Vue vue
<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>
React tsx
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 { makeData } from '../composables/makeData';
import { currentTheme } from '../composables/useRandomData';
const { isDark } = currentTheme();
function ContextMenu() {
const source = useMemo(() => makeData(100), []);
const columns: ColumnRegular[] = useMemo(
() => [
{
name: '#',
prop: 'id',
size: 70,
pin: 'colPinStart',
},
{
name: '🍎 Fruit',
prop: 'name',
},
{
name: '💰 Price',
prop: 'price',
pin: 'colPinEnd',
},
],
[],
);
const plugins = [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;
Angular ts
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';
}
Config ts
import type {
ColumnContextMenuOpenContext,
ContextMenuActionContext,
ContextMenuConfig,
ContextMenuItem,
} from '@revolist/revogrid-pro';
import type { ColumnData, ColumnGrouping, ColumnProp, ColumnRegular } from '@revolist/revogrid';
// 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: 'fa-solid fa-copy',
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: 'fa-solid fa-cut',
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: 'fa-solid fa-paste',
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: 'fa-solid fa-arrow-up',
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: 'fa-solid fa-arrow-down',
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: 'fa-solid fa-trash',
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: 'fa-solid fa-arrow-up-a-z',
name: 'Sort ascending',
action: (_, __, ___, ____, context) => sortColumn(context, 'asc'),
},
{
icon: 'fa-solid fa-arrow-down-z-a',
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: 'fa-solid fa-thumbtack',
name: 'Pin column left',
action: (_, __, ___, ____, context) => updateColumn(context, column => ({
...column,
pin: 'colPinStart',
})),
},
{
icon: 'fa-solid fa-thumbtack',
name: 'Pin column right',
action: (_, __, ___, ____, context) => updateColumn(context, column => ({
...column,
pin: 'colPinEnd',
})),
},
{
icon: 'fa-solid fa-circle-info',
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: 'fa-solid fa-fingerprint',
name: 'ID column',
action: (_, __, ___, ____, context) => {
console.log('ID column context', getColumnContext(context));
},
},
...sortColumnItems,
];
const pinnedStartColumnItems: ContextMenuItem[] = [
...sortColumnItems,
{
icon: 'fa-solid fa-thumbtack',
name: 'Unpin left column',
action: (_, __, ___, ____, context) => updateColumn(context, column => {
delete column.pin;
return column;
}),
},
];
const pinnedEndColumnItems: ContextMenuItem[] = [
...sortColumnItems,
{
icon: 'fa-solid fa-thumbtack',
name: 'Unpin right column',
action: (_, __, ___, ____, context) => updateColumn(context, column => {
delete column.pin;
return column;
}),
},
{
icon: 'fa-solid fa-circle-info',
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,
});
},
},
];