diff --git a/.prettierignore b/.prettierignore
index a61667e120..8f569d1b18 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,4 +2,6 @@
_variables.scss
_variables-theme-material.scss
_variables-theme-salesforce.scss
-**/test/vitest-coverage/*.*
\ No newline at end of file
+**/test/vitest-coverage/*.*
+demos/vue/src/router/index.ts
+demos/react/src/examples/slickgrid/App.tsx
\ No newline at end of file
diff --git a/demos/aurelia/src/examples/slickgrid/custom-aureliaViewModelEditor.ts b/demos/aurelia/src/examples/slickgrid/custom-aureliaViewModelEditor.ts
index 008e259c40..1e291b379b 100644
--- a/demos/aurelia/src/examples/slickgrid/custom-aureliaViewModelEditor.ts
+++ b/demos/aurelia/src/examples/slickgrid/custom-aureliaViewModelEditor.ts
@@ -33,7 +33,7 @@ export class CustomAureliaViewModelEditor implements Editor {
elmBindingContext?: IBindingContext;
constructor(private args: any) {
- this.grid = args && args.grid;
+ this.grid = args?.grid;
this.init();
}
diff --git a/demos/aurelia/src/examples/slickgrid/example10.ts b/demos/aurelia/src/examples/slickgrid/example10.ts
index 719d787265..8788d246e7 100644
--- a/demos/aurelia/src/examples/slickgrid/example10.ts
+++ b/demos/aurelia/src/examples/slickgrid/example10.ts
@@ -293,7 +293,7 @@ export class Example10 {
}
onGrid1SelectedRowsChanged(_e: Event, args: any) {
- const grid = args && args.grid;
+ const grid = args?.grid;
if (Array.isArray(args.rows)) {
this.selectedTitle = args.rows.map((idx: number) => {
const item = grid.getDataItem(idx);
diff --git a/demos/aurelia/src/examples/slickgrid/example2.ts b/demos/aurelia/src/examples/slickgrid/example2.ts
index 913fc8a181..6030b3f685 100644
--- a/demos/aurelia/src/examples/slickgrid/example2.ts
+++ b/demos/aurelia/src/examples/slickgrid/example2.ts
@@ -129,7 +129,7 @@ export class Example2 {
minWidth: 100,
formatter: customEnableButtonFormatter,
onCellClick: (_e, args) => {
- this.toggleCompletedProperty(args && args.dataContext);
+ this.toggleCompletedProperty(args?.dataContext);
},
},
];
diff --git a/demos/aurelia/src/examples/slickgrid/example23.ts b/demos/aurelia/src/examples/slickgrid/example23.ts
index 399643c719..989b13e618 100644
--- a/demos/aurelia/src/examples/slickgrid/example23.ts
+++ b/demos/aurelia/src/examples/slickgrid/example23.ts
@@ -259,11 +259,11 @@ export class Example23 {
}
refreshMetrics(_e: Event, args: any) {
- if (args && args.current >= 0) {
+ if (args?.current >= 0) {
setTimeout(() => {
this.metrics = {
startTime: new Date(),
- itemCount: (args && args.current) || 0,
+ itemCount: args?.current || 0,
totalItemCount: this.dataset.length || 0,
};
});
diff --git a/demos/aurelia/src/examples/slickgrid/example24.ts b/demos/aurelia/src/examples/slickgrid/example24.ts
index 4267b8b91c..a24fd42b0d 100644
--- a/demos/aurelia/src/examples/slickgrid/example24.ts
+++ b/demos/aurelia/src/examples/slickgrid/example24.ts
@@ -428,7 +428,7 @@ export class Example24 {
// optionally and conditionally define when the the menu is usable,
// this should be used with a custom formatter to show/hide/disable the menu
menuUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return dataContext.id < 21; // say we want to display the menu only from Task 0 to 20
},
// which column to show the command list? when not defined it will be shown over all columns
@@ -459,7 +459,7 @@ export class Example24 {
},
// only show command to 'Help' when the task is Not Completed
itemVisibilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
},
@@ -525,7 +525,7 @@ export class Example24 {
textCssClass: 'italic',
// only enable this option when the task is Not Completed
itemUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
// you can use the 'action' callback and/or subscribe to the 'onCallback' event, they both have the same arguments
@@ -547,7 +547,7 @@ export class Example24 {
disabled: true,
// only shown when the task is Not Completed
itemVisibilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
},
@@ -578,7 +578,7 @@ export class Example24 {
// subscribe to Context Menu onOptionSelected event (or use the action callback on each option)
onOptionSelected: (_e: any, args: any) => {
// change Priority
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext?.hasOwnProperty('priority')) {
dataContext.priority = args.item.option;
this.aureliaGrid.gridService.updateItem(dataContext);
diff --git a/demos/aurelia/src/examples/slickgrid/example3.ts b/demos/aurelia/src/examples/slickgrid/example3.ts
index 87aae58839..cb556556d1 100644
--- a/demos/aurelia/src/examples/slickgrid/example3.ts
+++ b/demos/aurelia/src/examples/slickgrid/example3.ts
@@ -28,7 +28,7 @@ const NB_ITEMS = 100;
// you can create custom validator to pass to an inline editor
const myCustomTitleValidator: EditorValidator = (value: any) => {
// you can get the Editor Args which can be helpful, e.g. we can get the Translate Service from it
- // const grid = args && args.grid;
+ // const grid = args?.grid;
// const gridOptions = grid.getOptions() as GridOption;
// const i18n = gridOptions.i18n;
diff --git a/demos/aurelia/src/examples/slickgrid/example33.ts b/demos/aurelia/src/examples/slickgrid/example33.ts
index d54398ca5e..0371f6f92c 100644
--- a/demos/aurelia/src/examples/slickgrid/example33.ts
+++ b/demos/aurelia/src/examples/slickgrid/example33.ts
@@ -459,7 +459,7 @@ export class Example33 {
onCommand: (e, args) => this.executeCommand(e, args),
onOptionSelected: (_e, args) => {
// change "Completed" property with new option selected from the Cell Menu
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext && dataContext.hasOwnProperty('completed')) {
dataContext.completed = args.item.option;
this.aureliaGrid.gridService.updateItem(dataContext);
diff --git a/demos/aurelia/src/examples/slickgrid/example4.ts b/demos/aurelia/src/examples/slickgrid/example4.ts
index 8613c2fbf5..947053c703 100644
--- a/demos/aurelia/src/examples/slickgrid/example4.ts
+++ b/demos/aurelia/src/examples/slickgrid/example4.ts
@@ -312,12 +312,12 @@ export class Example4 {
}
refreshMetrics(_e: Event, args: any) {
- if (args && args.current >= 0) {
+ if (args?.current >= 0) {
setTimeout(() => {
this.metrics = {
startTime: new Date(),
endTime: new Date(),
- itemCount: (args && args.current) || 0,
+ itemCount: args?.current || 0,
totalItemCount: this.dataset.length || 0,
};
});
diff --git a/demos/aurelia/src/examples/slickgrid/example51.html b/demos/aurelia/src/examples/slickgrid/example51.html
new file mode 100644
index 0000000000..5255b2d467
--- /dev/null
+++ b/demos/aurelia/src/examples/slickgrid/example51.html
@@ -0,0 +1,75 @@
+
+
+ Example 51: Menus with Slots
+
+
+ code
+
+
+
+
+
+
+
+
+
+ Menu Slots Demo with Custom Renderer
+
+
+ Click on the menu buttons to see the new single slot functionality working across all menu types (Header Menu, Cell
+ Menu, Context Menu, Grid Menu):
+
+
+ Note: The demo focuses on the custom rendering capability via slotRenderer and
+ defaultMenuItemRenderer, which work across all menu plugins (SlickHeaderMenu, SlickCellMenu, SlickContextMenu,
+ SlickGridMenu). Also note that the keyboard shortcuts displayed in the menus (e.g., Alt+↑, F5) are for
+ demo purposes only and do not actually trigger any actions.
+
+
+ Example 51: Menus with Slots
+
+ see
+
+ code
+
+
+
+
+
+
+
+
+ Menu Slots Demo with Custom Renderer
+
+
+ Click on the menu buttons to see the new single slot functionality working across all menu types (Header Menu,
+ Cell Menu, Context Menu, Grid Menu):
+
+
+
+ Note: The demo focuses on the custom rendering capability via slotRenderer and
+ defaultMenuItemRenderer, which work across all menu plugins (SlickHeaderMenu, SlickCellMenu, SlickContextMenu,
+ SlickGridMenu). Also note that the keyboard shortcuts displayed in the menus (e.g., Alt+↑, F5) are for
+ demo purposes only and do not actually trigger any actions.
+
+
+ Click on the menu buttons to see the new single slot functionality working across all menu types (Header Menu, Cell
+ Menu, Context Menu, Grid Menu):
+
+
+ Note: The demo focuses on the custom rendering capability via slotRenderer and
+ defaultMenuItemRenderer, which work across all menu plugins (SlickHeaderMenu, SlickCellMenu, SlickContextMenu,
+ SlickGridMenu). Also note that the keyboard shortcuts displayed in the menus (e.g., Alt+↑, F5) are for demo
+ purposes only and do not actually trigger any actions.
+
+
+ `,
+ action: () => alert('Export to CSV'),
+ },
+ {
+ command: 'refresh-data',
+ title: 'Refresh Data',
+ iconCssClass: 'mdi mdi-refresh',
+ // Demo: slotRenderer with keyboard shortcut
+ slotRenderer: (cmdItem) => {
+ // you can use `createDomElement()` from Slickgrid for easier DOM element creation
+ const menuItemElm = createDomElement('div', { className: 'menu-item' });
+ const iconElm = createDomElement('i', { className: `${cmdItem.iconCssClass} menu-item-icon` });
+ const menuItemLabelElm = createDomElement('span', { className: 'menu-item-label', textContent: cmdItem.title || '' });
+ const kbdElm = createDomElement('kbd', { className: 'key-hint', textContent: 'F5' });
+ menuItemElm.appendChild(iconElm);
+ menuItemElm.appendChild(menuItemLabelElm);
+ menuItemElm.appendChild(kbdElm);
+ return menuItemElm;
+ },
+ action: () => alert('Refresh data'),
+ },
+ ] as Array;
+ },
+ },
+
+ // tooltip plugin
+ externalResources: [new SlickCustomTooltip()],
+ customTooltip: {
+ observeAllTooltips: true,
+ },
+ };
+ }
+
+ clearGrouping() {
+ this.sgb?.dataView?.setGrouping([]);
+ }
+
+ collapseAllGroups() {
+ this.sgb?.dataView?.collapseAllGroups();
+ }
+
+ expandAllGroups() {
+ this.sgb?.dataView?.expandAllGroups();
+ }
+
+ groupByDuration() {
+ // you need to manually add the sort icon(s) in UI
+ this.sgb?.slickGrid?.setSortColumns([{ columnId: 'duration', sortAsc: true }]);
+ this.sgb?.dataView?.setGrouping({
+ getter: 'duration',
+ formatter: (g) => `Duration: ${g.value} (${g.count} items)`,
+ comparer: (a, b) => SortComparers.numeric(a.value, b.value, SortDirectionNumber.asc),
+ aggregators: [new Aggregators.Avg('percentComplete'), new Aggregators.Sum('cost')],
+ aggregateCollapsed: false,
+ lazyTotalsCalculation: true,
+ } as Grouping);
+ this.sgb?.slickGrid?.invalidate(); // invalidate all rows and re-render
+ }
+
+ loadData(count: number): ReportItem[] {
+ const tmpData: ReportItem[] = [];
+ for (let i = 0; i < count; i++) {
+ const randomDuration = Math.round(Math.random() * 100);
+ const randomYear = 2000 + Math.floor(Math.random() * 10);
+ const randomMonth = Math.floor(Math.random() * 11);
+ const randomDay = Math.floor(Math.random() * 29);
+ const randomPercent = Math.round(Math.random() * 100);
+
+ tmpData[i] = {
+ id: i,
+ title: 'Task ' + i,
+ duration: randomDuration,
+ cost: Math.round(Math.random() * 10000) / 100,
+ percentComplete: randomPercent,
+ start: new Date(randomYear, randomMonth, randomDay),
+ finish: new Date(randomYear, randomMonth + 1, randomDay),
+ };
+ }
+ return tmpData;
+ }
+
+ toggleSubTitle() {
+ this.subTitleStyle = this.subTitleStyle === 'display: block' ? 'display: none' : 'display: block';
+ this.sgb.resizerService.resizeGrid();
+ }
+}
diff --git a/demos/vue/src/components/Example03.vue b/demos/vue/src/components/Example03.vue
index 7873927bbe..1b6472f73c 100644
--- a/demos/vue/src/components/Example03.vue
+++ b/demos/vue/src/components/Example03.vue
@@ -41,7 +41,7 @@ let vueGrid!: SlickgridVueInstance;
// you can create custom validator to pass to an inline editor
const myCustomTitleValidator: EditorValidator = (value: any) => {
// you can get the Editor Args which can be helpful, e.g. we can get the Translate Service from it
- // const grid = args && args.grid;
+ // const grid = args?.grid;
// const gridOptions = grid.getOptions() as GridOption;
// const i18n = gridOptions.i18n;
diff --git a/demos/vue/src/components/Example04.vue b/demos/vue/src/components/Example04.vue
index ed74630b1a..7cd39768d3 100644
--- a/demos/vue/src/components/Example04.vue
+++ b/demos/vue/src/components/Example04.vue
@@ -308,12 +308,12 @@ function setSortingDynamically() {
}
function refreshMetrics(_e: Event, args: any) {
- if (args && args.current >= 0) {
+ if (args?.current >= 0) {
setTimeout(() => {
metrics.value = {
startTime: new Date(),
endTime: new Date(),
- itemCount: (args && args.current) || 0,
+ itemCount: args?.current || 0,
totalItemCount: dataset.value.length || 0,
};
});
diff --git a/demos/vue/src/components/Example24.vue b/demos/vue/src/components/Example24.vue
index f383cdeafa..05ff6fc8e1 100644
--- a/demos/vue/src/components/Example24.vue
+++ b/demos/vue/src/components/Example24.vue
@@ -424,7 +424,7 @@ function getContextMenuOptions(): ContextMenu {
// optionally and conditionally define when the the menu is usable,
// this should be used with a custom formatter to show/hide/disable the menu
menuUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return dataContext.id < 21; // say we want to display the menu only from Task 0 to 20
},
// which column to show the command list? when not defined it will be shown over all columns
@@ -455,7 +455,7 @@ function getContextMenuOptions(): ContextMenu {
},
// only show command to 'Help' when the task is Not Completed
itemVisibilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
},
@@ -521,7 +521,7 @@ function getContextMenuOptions(): ContextMenu {
textCssClass: 'italic',
// only enable this option when the task is Not Completed
itemUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
// you can use the 'action' callback and/or subscribe to the 'onCallback' event, they both have the same arguments
@@ -543,7 +543,7 @@ function getContextMenuOptions(): ContextMenu {
disabled: true,
// only shown when the task is Not Completed
itemVisibilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
},
@@ -574,7 +574,7 @@ function getContextMenuOptions(): ContextMenu {
// subscribe to Context Menu onOptionSelected event (or use the action callback on each option)
onOptionSelected: (_e: any, args: any) => {
// change Priority
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext && 'priority' in dataContext) {
dataContext.priority = args.item.option;
vueGrid.gridService.updateItem(dataContext);
diff --git a/demos/vue/src/components/Example33.vue b/demos/vue/src/components/Example33.vue
index 89b8f0d5c5..0eae92d27a 100644
--- a/demos/vue/src/components/Example33.vue
+++ b/demos/vue/src/components/Example33.vue
@@ -497,7 +497,7 @@ function defineGrid() {
onCommand: (e, args) => executeCommand(e, args),
onOptionSelected: (_e, args) => {
// change "Completed" property with new option selected from the Cell Menu
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext && 'completed' in dataContext) {
dataContext.completed = args.item.option;
vueGrid.gridService.updateItem(dataContext);
diff --git a/demos/vue/src/components/Example51.vue b/demos/vue/src/components/Example51.vue
new file mode 100644
index 0000000000..c1a5389ea2
--- /dev/null
+++ b/demos/vue/src/components/Example51.vue
@@ -0,0 +1,818 @@
+
+
+
+
+ Example 51: Menus with Slots
+
+ see
+
+ code
+
+
+
+
+
+
+
+
+ Menu Slots Demo with Custom Renderer
+
+
+ Click on the menu buttons to see the new single slot functionality working across all menu types (Header Menu, Cell
+ Menu, Context Menu, Grid Menu):
+
+
+ Note: The demo focuses on the custom rendering capability via slotRenderer and
+ defaultMenuItemRenderer, which work across all menu plugins (SlickHeaderMenu, SlickCellMenu, SlickContextMenu,
+ SlickGridMenu). Also note that the keyboard shortcuts displayed in the menus (e.g., Alt+↑, F5) are for
+ demo purposes only and do not actually trigger any actions.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/vue/src/router/index.ts b/demos/vue/src/router/index.ts
index 9b49903f2c..c699a3ecd2 100644
--- a/demos/vue/src/router/index.ts
+++ b/demos/vue/src/router/index.ts
@@ -1,5 +1,5 @@
-import type { RouteRecordRaw } from 'vue-router';
-import { createRouter, createWebHashHistory } from 'vue-router';
+import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router';
+
import Example1 from '../components/Example01.vue';
import Example2 from '../components/Example02.vue';
import Example3 from '../components/Example03.vue';
@@ -50,6 +50,7 @@ import Example47 from '../components/Example47.vue';
import Example48 from '../components/Example48.vue';
import Example49 from '../components/Example49.vue';
import Example50 from '../components/Example50.vue';
+import Example51 from '../components/Example51.vue';
import Home from '../Home.vue';
export const routes: RouteRecordRaw[] = [
@@ -105,6 +106,7 @@ export const routes: RouteRecordRaw[] = [
{ path: '/example48', name: '48- Hybrid Selection Model', component: Example48 },
{ path: '/example49', name: '49- Spreadsheet Drag-Fill', component: Example49 },
{ path: '/example50', name: '50- Master/Detail Grids', component: Example50 },
+ { path: '/example51', name: '51- Menus with Slots', component: Example51 },
];
export const router = createRouter({
diff --git a/demos/vue/test/cypress/e2e/example51.cy.ts b/demos/vue/test/cypress/e2e/example51.cy.ts
new file mode 100644
index 0000000000..80e5148c97
--- /dev/null
+++ b/demos/vue/test/cypress/e2e/example51.cy.ts
@@ -0,0 +1,382 @@
+import { format } from '@formkit/tempo';
+
+describe('Example 51 - Menus with Slots', () => {
+ const fullTitles = ['Title', 'Duration', 'Start', 'Finish', 'Cost', '% Complete', 'Action'];
+
+ it('should display Example title', () => {
+ cy.visit(`${Cypress.config('baseUrl')}/example51`);
+ cy.get('h2').should('contain', 'Example 51: Menus with Slots');
+ });
+
+ it('should have exact column titles in the grid', () => {
+ cy.get('#grid51')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(fullTitles[index]));
+ });
+
+ it('should open Context Menu hover "Duration" column and expect built-in and custom items listed in specific order', () => {
+ cy.get('[data-row="0"] > .slick-cell:nth(2)').rightclick({ force: true });
+
+ // 1st item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('.edit-cell-icon').contains('✎');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item span').contains('Edit Cell');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('kbd.edit-cell').contains('F2');
+
+ // icon should rotate while hovering
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0)').trigger('mouseover');
+ cy.wait(175); // wait for rotation
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item .edit-cell-icon')
+ .invoke('css', 'transform') // Get the transform property
+ .then((cssTransform) => {
+ const transformValue = cssTransform as unknown as string; // Cast to string
+ cy.getTransformValue(transformValue, true, 'rotate').then((rotationAngle) => {
+ expect(rotationAngle).to.approximately(13, 15); // 15 degrees rotation
+ });
+ });
+
+ // 2nd item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item').find('i.mdi-content-copy').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item').find('span.menu-item-label').contains('Copy');
+
+ // 3rd item - divider
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(2)').should('have.class', 'slick-menu-item-divider');
+
+ // 4th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(3)').should('have.class', 'slick-menu-item-disabled');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-close').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Clear all Grouping');
+
+ // 5th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-disabled');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item').find('i.mdi-arrow-collapse').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Collapse all Groups');
+
+ // 6th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(5)').should('have.class', 'slick-menu-item-disabled');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item').find('i.mdi-arrow-expand').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Expand all Groups');
+
+ // 7th item - divider
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(6)').should('have.class', 'slick-menu-item-divider');
+
+ // 8th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item').find('i.mdi-download').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Export');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(7)').find('.sub-item-chevron').should('exist');
+
+ // 9th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(8)').should('have.class', 'slick-menu-item-divider');
+
+ // 10th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(9) .menu-item')
+ .find('i.mdi-delete.text-danger')
+ .should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(9) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Delete Row');
+ });
+
+ it('should open Export->Excel context sub-menu', () => {
+ const subCommands1 = ['Export as Excel', 'Export as CSV', 'Export as PDF'];
+
+ const stub = cy.stub();
+ cy.on('window:alert', stub);
+
+ cy.get('[data-row="0"] > .slick-cell:nth(2)').should('contain', '0');
+ cy.get('[data-row="0"] > .slick-cell:nth(2)').rightclick({ force: true });
+
+ cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains(/^Export$/)
+ .trigger('mouseover');
+
+ cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list')
+ .should('exist')
+ .find('.slick-menu-item .menu-item')
+ .each(($command, index) => expect($command.text()).to.contain(subCommands1[index]));
+
+ // click different sub-menu
+ cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains('Export as Excel')
+ .should('exist')
+ .click()
+ .then(() => expect(stub.getCall(0)).to.be.calledWith('Export to Excel'));
+
+ cy.get('.slick-submenu').should('have.length', 0);
+ });
+
+ it('should open Header Menu from the "Title" column and expect some commands to have keyboard hints on the right side', () => {
+ cy.get('.slick-header-column:nth(0)').trigger('mouseover', { force: true });
+ cy.get('.slick-header-column:nth(0)').children('.slick-header-menu-button').invoke('show').click();
+
+ // 1st item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('i.mdi-arrow-expand-horizontal')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Resize by Content');
+
+ // 2nd item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(1)').should('have.class', 'slick-menu-item-divider');
+
+ // 3rd item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('i.mdi-sort-ascending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Ascending');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('kbd.key-hint').contains('Alt+↑');
+
+ // 4th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-sort-descending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Descending');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('kbd.key-hint').contains('Alt+↓');
+
+ // 5th item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-divider');
+
+ // 6th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('i.mdi-filter-remove-outline')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Filter');
+
+ // 7th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('i.mdi-sort-variant-off').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Sort');
+
+ // 8th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item').find('i.mdi-close').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Hide Column');
+ });
+
+ it('should open Header Menu from the "Duration" column and expect some commands to have tags on the right side', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover', { force: true });
+ cy.get('.slick-header-column:nth(1)').children('.slick-header-menu-button').invoke('show').click();
+
+ // 1st item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('i.mdi-arrow-expand-horizontal')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Resize by Content');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('span.key-hint.danger').contains('NEW');
+
+ // 2nd item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(1)').should('have.class', 'slick-menu-item-divider');
+
+ // 3rd item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('i.mdi-sort-ascending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Ascending');
+
+ // 4th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-sort-descending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Descending');
+
+ // 5th item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-divider');
+
+ // 6th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('i.mdi-filter-remove-outline')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Filter');
+
+ // 7th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('i.mdi-sort-variant-off').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Sort');
+
+ // 8th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item').find('i.mdi-close').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Hide Column');
+ });
+
+ it('should open Header Menu from the "Cost" column and expect first item to have a dynamic tooltip timestamp when hovering', () => {
+ cy.get('#grid51').find('.slick-header-column:nth(4)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click();
+
+ // 1st item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('.advanced-export-icon').contains('📊');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').contains('Advanced Export');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('kbd.key-hint').contains('Ctrl+E');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').should(
+ 'have.css',
+ 'background-color',
+ 'rgba(0, 0, 0, 0)'
+ );
+
+ // icon should scale up
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').trigger('mouseover');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item .advanced-export-icon')
+ .invoke('css', 'transform')
+ .then((cssTransform) => {
+ const transformValue = cssTransform as unknown as string; // Cast to string
+ cy.getTransformValue(transformValue, true, 'scale').then((scaleValue) => {
+ expect(scaleValue).to.be.approximately(1.1, 1.15); // Check the scale value if applied
+ });
+ });
+
+ const today = format(new Date(), 'YYYY-MM-DD');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').should(
+ 'have.css',
+ 'background-color',
+ 'rgb(133, 70, 133)'
+ );
+ cy.get('.slick-custom-tooltip').contains(`📈 Export timestamp: ${today}`);
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').trigger('mouseout');
+ cy.get('.slick-custom-tooltip').should('not.exist');
+ cy.get('body').click();
+ });
+
+ it('should open Action Menu from last column "Action" column and expect custom items listed in specific order', () => {
+ cy.get('[data-row="1"] > .slick-cell:nth(6)').click();
+ cy.get('.slick-command-header.with-title.with-close').contains('Cell Actions');
+
+ // 1st item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('.mdi-content-copy').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item span').contains('Copy Cell Value');
+
+ // 2nd item - divider
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(1)').should('have.class', 'slick-menu-item-divider');
+
+ // 3rd item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('.mdi-download').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item span').contains('Export Row');
+
+ // 4th item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-download').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('span.menu-item-label').contains('Export');
+
+ // 5th item - divider
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-divider');
+
+ // 6th item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item').find('.edit-cell-icon').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item .edit-cell-icon').contains('✎');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(5)').should('have.css', 'background-color', 'rgba(0, 0, 0, 0)');
+
+ // 7th item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('.mdi-delete.text-danger').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item span').contains('Delete Row');
+ });
+
+ it('should open Export->Excel cell sub-menu', () => {
+ const subCommands1 = ['Export as Excel', 'Export as CSV', 'Export as PDF'];
+
+ const stub = cy.stub();
+ cy.on('window:alert', stub);
+
+ cy.get('.slick-cell-menu.slick-menu-level-0 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains(/^Export$/)
+ .trigger('mouseover');
+
+ cy.get('.slick-cell-menu.slick-menu-level-1 .slick-menu-command-list')
+ .should('exist')
+ .find('.slick-menu-item .menu-item')
+ .each(($command, index) => expect($command.text()).to.contain(subCommands1[index]));
+
+ // click different sub-menu
+ cy.get('.slick-cell-menu.slick-menu-level-1 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains('Export as Excel')
+ .should('exist')
+ .click()
+ .then(() => expect(stub.getCall(0)).to.be.calledWith('Export row #1 to Excel'));
+
+ cy.get('.slick-submenu').should('have.length', 0);
+ });
+
+ it('should open Grid Menu and expect built-in commands first then custom items listed in specific order', () => {
+ cy.get('.slick-grid-menu-button.mdi-menu').click();
+
+ // 1st item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('.mdi-filter-remove-outline.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item span').contains('Clear all Filters');
+
+ // 2nd item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item')
+ .find('.mdi-sort-variant-off.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item span').contains('Clear all Sorting');
+
+ // 3rd item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item')
+ .find('.mdi-flip-vertical.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item span').contains('Toggle Filter Row');
+
+ // 4th item - divider
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(3)').should('have.class', 'slick-menu-item-divider');
+
+ // 5th item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item')
+ .find('.mdi-file-excel-outline.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item span').contains('Export to Excel');
+
+ // 6th item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('.mdi-download.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item span').contains('Export to CSV');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item').find('span.key-hint.warn').contains('CUSTOM');
+
+ // 7th item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item')
+ .find('.mdi-refresh.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item span').contains('Refresh Data');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('kbd.key-hint').contains('F5');
+ });
+
+ it('should sort ascending "Duration" even though the header menu item was override without an action callback', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click();
+
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').should('contain', 'Sort Ascending').click();
+
+ cy.get('[data-row=0]').children('.slick-cell:nth(1)').should('contain', '0');
+ cy.get('[data-row=1]').children('.slick-cell:nth(1)').should('contain', '0');
+ cy.get('[data-row=2]').children('.slick-cell:nth(1)').should('contain', '0');
+ });
+
+ it('should sort descending "Duration" even though the header menu item was override without an action callback', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click();
+
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').should('contain', 'Sort Descending').click();
+
+ cy.get('[data-row=0]').children('.slick-cell:nth(1)').should('contain', '100');
+ cy.get('[data-row=1]').children('.slick-cell:nth(1)').should('contain', '100');
+ cy.get('[data-row=2]').children('.slick-cell:nth(1)').should('contain', '100');
+ });
+});
diff --git a/demos/vue/test/cypress/support/commands.ts b/demos/vue/test/cypress/support/commands.ts
index a35be6228b..3be5a3685b 100644
--- a/demos/vue/test/cypress/support/commands.ts
+++ b/demos/vue/test/cypress/support/commands.ts
@@ -47,6 +47,7 @@ declare global {
): Chainable>;
saveLocalStorage: () => void;
restoreLocalStorage: () => void;
+ getTransformValue(cssTransformMatrix: string, absoluteValue: boolean, transformType?: 'rotate' | 'scale'): Chainable;
}
}
}
@@ -86,3 +87,35 @@ Cypress.Commands.add('restoreLocalStorage', () => {
localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
});
});
+
+Cypress.Commands.add(
+ 'getTransformValue',
+ (
+ cssTransformMatrix: string,
+ absoluteValue: boolean,
+ transformType: 'rotate' | 'scale' = 'rotate' // Default to 'rotate'
+ ): Cypress.Chainable => {
+ if (!cssTransformMatrix || cssTransformMatrix === 'none') {
+ throw new Error('Transform matrix is undefined or none');
+ }
+
+ const cssTransformMatrixIndexes = cssTransformMatrix.split('(')[1].split(')')[0].split(',');
+
+ if (transformType === 'rotate') {
+ const cssTransformScale = Math.sqrt(
+ +cssTransformMatrixIndexes[0] * +cssTransformMatrixIndexes[0] + +cssTransformMatrixIndexes[1] * +cssTransformMatrixIndexes[1]
+ );
+
+ const cssTransformSin = +cssTransformMatrixIndexes[1] / cssTransformScale;
+ const cssTransformAngle = Math.round(Math.asin(cssTransformSin) * (180 / Math.PI));
+
+ return cy.wrap(absoluteValue ? Math.abs(cssTransformAngle) : cssTransformAngle);
+ } else if (transformType === 'scale') {
+ // Assuming scale is based on the first value in the matrix.
+ const scaleValue = +cssTransformMatrixIndexes[0]; // First value typically represents scaling in x direction.
+ return cy.wrap(scaleValue); // Directly return the scale value.
+ }
+
+ throw new Error('Unsupported transform type');
+ }
+);
diff --git a/docs/TOC.md b/docs/TOC.md
index 492f4797d4..4322fd7f46 100644
--- a/docs/TOC.md
+++ b/docs/TOC.md
@@ -46,6 +46,7 @@
* [Resize by Cell Content](grid-functionalities/resize-by-cell-content.md)
* [Column Picker](grid-functionalities/column-picker.md)
* [Composite Editor Modal](grid-functionalities/composite-editor-modal.md)
+* [Custom Menu Slots](menu-slots.md)
* [Custom Tooltip](grid-functionalities/custom-tooltip.md)
* [Column & Row Spanning](grid-functionalities/column-row-spanning.md)
* [Context Menu](grid-functionalities/context-menu.md)
diff --git a/docs/column-functionalities/cell-menu.md b/docs/column-functionalities/cell-menu.md
index 8dbbb14c5c..533230cb14 100644
--- a/docs/column-functionalities/cell-menu.md
+++ b/docs/column-functionalities/cell-menu.md
@@ -94,7 +94,8 @@ So if you decide to use the `action` callback, then your code would look like th
##### with `action` callback
```ts
this.columnDefinitions = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
commandItems: [
{ command: 'command1', title: 'Command 1', action: (e, args) => console.log(args) },
@@ -111,7 +112,8 @@ The `onCommand` (or `onOptionSelected`) **must** be defined in the Grid Options
```ts
this.columnDefinitions = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
commandItems: [
{ command: 'command1', title: 'Command 1' },
@@ -140,6 +142,11 @@ this.gridOptions = {
};
```
+### Custom Menu Item Rendering
+For advanced customization of menu item appearance, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks to create custom HTML or HTMLElement content for your menu items. This allows you to add badges, keyboard shortcuts, status indicators, and more.
+
+See [Custom Menu Slots](../../menu-slots.md) for detailed examples and best practices on rendering custom menu item content across all menu types.
+
### Override Callback Methods
What if you want to dynamically disable or hide a Command/Option or even disable the entire menu in certain circumstances? For these cases, you would use the override callback methods, the method must return a `boolean`. The list of override available are the following
- `menuUsabilityOverride` returning false would make the Cell Menu unavailable to the user
@@ -150,12 +157,10 @@ What if you want to dynamically disable or hide a Command/Option or even disable
For example, say we want the Cell Menu to only be available on the first 20 rows of the grid, we could use the override this way
```ts
this.columnDefinitions = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
- menuUsabilityOverride: (args) => {
- const dataContext = args?.dataContext;
- return (dataContext.id < 21); // say we want to display the menu only from Task 0 to 20
- },
+ menuUsabilityOverride: (args) => args?.dataContext.id < 21, // say we want to display the menu only from Task 0 to 20
}
}
];
@@ -163,29 +168,34 @@ this.columnDefinitions = [
To give another example, with Options this time, we could say that we enable the `n/a` option only when the row is Completed. So we could do it this way
```ts
-this.columnDefinitions = [{
- id: 'action', field: 'action', name: 'Action',
- cellMenu: {
- optionItems: [{
- option: 0, title: 'n/a', textCssClass: 'italic',
- // only enable this option when the task is Not Completed
- itemUsabilityOverride: (args) => {
- const dataContext = args?.dataContext;
- return !dataContext.completed;
- },
- { option: 1, iconCssClass: 'mdi mdi-star-outline yellow', title: 'Low' },
- { option: 2, iconCssClass: 'mdi mdi-star orange', title: 'Medium' },
- { option: 3, iconCssClass: 'mdi mdi-star red', title: 'High' },
- }]
- }
-}];
+this.columnDefinitions = [
+ {
+ id: 'action', field: 'action', name: 'Action',
+ cellMenu: {
+ optionItems: [
+ {
+ option: 0, title: 'n/a', textCssClass: 'italic',
+ // only enable this option when the task is Not Completed
+ itemUsabilityOverride: (args) => {
+ const dataContext = args?.dataContext;
+ return !dataContext.completed;
+ },
+ },
+ { option: 1, iconCssClass: 'mdi mdi-star-outline yellow', title: 'Low' },
+ { option: 2, iconCssClass: 'mdi mdi-star orange', title: 'Medium' },
+ { option: 3, iconCssClass: 'mdi mdi-star red', title: 'High' },
+ ]
+ }
+ },
+];
```
### How to add Translations?
It works exactly like the rest of the library when `enableTranslate` is set, all we have to do is to provide translations with the `Key` suffix, so for example without translations, we would use `title` and that would become `titleKey` with translations, that;'s easy enough. So for example, a list of Options could be defined as follow:
```ts
this.columnDefinitions = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
optionTitleKey: 'OPTIONS', // optionally pass a title to show over the Options
optionItems: [
diff --git a/docs/column-functionalities/editors.md b/docs/column-functionalities/editors.md
index 27d9c8db27..21f176002b 100644
--- a/docs/column-functionalities/editors.md
+++ b/docs/column-functionalities/editors.md
@@ -270,7 +270,7 @@ So if we take all of these informations and we want to create our own Custom Edi
const myCustomTitleValidator: EditorValidator = (value: any, args: EditorArgs) => {
// you can get the Editor Args which can be helpful, e.g. we can get the Translate Service from it
const grid = args?.grid;
- const gridOptions = (grid && grid.getOptions) ? grid.getOptions() : {};
+ const gridOptions = grid.getOptions() : {};
const i18n = gridOptions.i18n;
if (value == null || value === undefined || !value.length) {
diff --git a/docs/column-functionalities/editors/select-dropdown-editor.md b/docs/column-functionalities/editors/select-dropdown-editor.md
index defd751b7a..eef11e25c5 100644
--- a/docs/column-functionalities/editors/select-dropdown-editor.md
+++ b/docs/column-functionalities/editors/select-dropdown-editor.md
@@ -6,11 +6,11 @@
- [Collection Label Prefix/Suffix](#collection-label-prefixsuffix)
- [Collection Label Render HTML](#collection-label-render-html)
- [Collection Change Watch](#collection-watch)
- - [`multiple-select.js` Options](#multiple-selectjs-options)
+ - [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- See the [Editors - Docs](../Editors.md) for more general info about Editors (validators, event handlers, ...)
## Select Editors
-The library ships with two select editors: `singleSelectEditor` and the `multipleSelectEditor`. Both support the [multiple-select](https://github.com/ghiscoding/multiple-select-adapted/blob/master/src/multiple-select.js) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/collectionFormatter.ts).
+The library ships with two select editors: `singleSelectEditor` and the `multipleSelectEditor`. Both support the [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/collectionFormatter.ts).
We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla).
@@ -199,8 +199,8 @@ this.columnDefinitions = [
];
```
-### `multiple-select.js` Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+### `multiple-select-vanilla` Options
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/lib` folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -226,7 +226,7 @@ this.columnDefinitions = [
collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }],
model: Editors.singleSelect,
elementOptions: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
}
diff --git a/docs/column-functionalities/filters/select-filter.md b/docs/column-functionalities/filters/select-filter.md
index 5dcd583ac5..f0cecfd56a 100644
--- a/docs/column-functionalities/filters/select-filter.md
+++ b/docs/column-functionalities/filters/select-filter.md
@@ -16,7 +16,7 @@
- [Collection Async Load](#collection-async-load)
- [Collection Lazy Load](#collection-lazy-load)
- [Collection Watch](#collection-watch)
-- [`multiple-select.js` Options](#multiple-selectjs-options)
+- [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- [Filter Options (`MultipleSelectOption` interface)](#filter-options-multipleselectoption-interface)
- [Display shorter selected label text](#display-shorter-selected-label-text)
- [Query against a different field](#query-against-another-field-property)
@@ -33,7 +33,7 @@ Multiple Select (dropdown) filter is useful when we want to filter the grid 1 or
We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla).
#### Note
-For this filter to work you will need to add [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) to your project. This is a customized version of the original (thought all the original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options). Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/dist/lib` folder. This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original)
+For this filter to work you will need to add [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) to your project. This is a customized version of the original (thought all the original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options). Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/dist/lib` folder. This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original)
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
- `okButtonText` was also added for locale (i18n)
- `offsetLeft` option was added to make it possible to offset the dropdown. By default it is set to 0 and is aligned to the left of the select element. This option is particularly helpful when used as the last right column, not to fall off the screen.
@@ -608,8 +608,8 @@ this.gridOptions = {
}
```
-### Multiple-select.js Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+### multiple-select-vanilla Options
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit SlickGrid-Universal needs, which is why we are using a fork [ghiscoding/multiple-select-modified](https://github.com/ghiscoding/multiple-select-modified) folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -641,7 +641,7 @@ prepareGrid() {
model: Filters.singleSelect,
// previously known as `filterOptions` for < 9.0
options: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
} as MultipleSelectOption
diff --git a/docs/grid-functionalities/composite-editor-modal.md b/docs/grid-functionalities/composite-editor-modal.md
index 1619e6e7ee..dcf33b1f8b 100644
--- a/docs/grid-functionalities/composite-editor-modal.md
+++ b/docs/grid-functionalities/composite-editor-modal.md
@@ -535,7 +535,7 @@ export class GridExample {
// you can also change some editor options
// not all Editors supports this functionality, so far only these Editors are supported: AutoComplete, Date, Single/Multiple Select
if (columnDef.id === 'completed') {
- this.compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select.js, show filter in dropdown
+ this.compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select-vanilla, show filter in dropdown
this.compositeEditorInstance.changeFormEditorOption('product', 'minLength', 3); // autocomplete, change minLength char to type
this.compositeEditorInstance.changeFormEditorOption('finish', 'displayDateMin', 'today'); // vanilla calendar date picker, change minDate to today
}
diff --git a/docs/grid-functionalities/context-menu.md b/docs/grid-functionalities/context-menu.md
index 4b9ecd1b42..fc797f97de 100644
--- a/docs/grid-functionalities/context-menu.md
+++ b/docs/grid-functionalities/context-menu.md
@@ -21,7 +21,7 @@ This extensions is wrapped around the new SlickGrid Plugin **SlickContextMenu**
### Default Usage
Technically, the Context Menu is enabled by default (copy, export) and so you don't have anything to do to enjoy it (you could disable it at any time). However, if you want to customize the content of the Context Menu, then continue reading. You can customize the menu with 2 different lists, Commands and/or Options, they can be used separately or at the same time. Also note that even though the code shown below makes a separation between the Commands and Options, you can mix them in the same Context Menu.
-#### with Commands
+#### with Commands (Static)
```ts
this.gridOptions = {
enableFiltering: true,
@@ -60,6 +60,98 @@ this.gridOptions = {
};
```
+#### with Commands (Dynamic Builder)
+For advanced scenarios where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list.
+
+```ts
+this.gridOptions = {
+ enableContextMenu: true,
+ contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ // Example: Add custom commands after built-in ones
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'delete-row',
+ title: 'Delete Row',
+ iconCssClass: 'mdi mdi-delete text-danger',
+ action: (e, args) => {
+ if (confirm(`Delete row ${args.dataContext.id}?`)) {
+ this.gridService.deleteItem(args.dataContext);
+ }
+ }
+ },
+ {
+ command: 'duplicate-row',
+ title: 'Duplicate Row',
+ iconCssClass: 'mdi mdi-content-duplicate',
+ action: (e, args) => {
+ const newItem = { ...args.dataContext, id: this.generateNewId() };
+ this.gridService.addItem(newItem);
+ }
+ }
+ ];
+ },
+ onCommand: (e, args) => {
+ // Handle commands here if not using action callbacks
+ console.log('Command:', args.command);
+ }
+ }
+};
+```
+
+**Example: Filter commands based on row data**
+```ts
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ // You can't access row data here, but you can filter/modify built-in items
+ // Use itemUsabilityOverride or itemVisibilityOverride for row-specific logic
+
+ // Only show export commands
+ return builtInItems.filter(item =>
+ item === 'divider' || item.command?.includes('export')
+ );
+ }
+}
+```
+
+**Example: Sort and reorganize commands**
+```ts
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ const customFirst = [
+ {
+ command: 'edit',
+ title: 'Edit Row',
+ iconCssClass: 'mdi mdi-pencil',
+ positionOrder: 0
+ }
+ ];
+
+ // Sort built-in commands by positionOrder
+ const sorted = [...builtInItems].sort((a, b) => {
+ if (a === 'divider' || b === 'divider') return 0;
+ return (a.positionOrder || 50) - (b.positionOrder || 50);
+ });
+
+ return [...customFirst, 'divider', ...sorted];
+ }
+}
+```
+
+**When to use `commandListBuilder` vs `commandItems`:**
+- Use `commandItems` for static command lists
+- Use `commandListBuilder` when you need to:
+ - Append/prepend to built-in commands
+ - Filter or modify commands dynamically
+ - Sort or reorder the final command list
+ - Have full control over what gets rendered
+
+**Note:** Typically use `commandListBuilder` **instead of** `commandItems`, not both together.
+
+See the main [Custom Menu Slots](../menu-slots.md) documentation for detailed `commandListBuilder` examples.
+
#### with Options
That is when you want to define a list of Options (only 1 list) that the user can choose from and once is selected we would do something (for example change the value of a cell in the grid).
```ts
@@ -122,6 +214,11 @@ contextMenu: {
}
```
+### Custom Menu Item Rendering
+For advanced customization of menu item appearance, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks to create custom HTML or HTMLElement content for your menu items. This allows you to add badges, keyboard shortcuts, status indicators, and more.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content across all menu types.
+
### Override Callback Methods
What if you want to dynamically disable or hide a Command/Option or even disable the entire menu in certain circumstances? For these cases, you would use the override callback methods, the method must return a `boolean`. The list of override available are the following
- `menuUsabilityOverride` returning false would make the Context Menu unavailable to the user
@@ -141,17 +238,19 @@ contextMenu: {
To give another example, with Options this time, we could say that we enable the `n/a` option only when the row is Completed. So we could do it this way
```ts
contextMenu: {
- optionItems: [{
- option: 0, title: 'n/a', textCssClass: 'italic',
- // only enable this option when the task is Not Completed
- itemUsabilityOverride: (args) => {
- const dataContext = args?.dataContext;
- return !dataContext.completed;
+ optionItems: [
+ {
+ option: 0, title: 'n/a', textCssClass: 'italic',
+ // only enable this option when the task is Not Completed
+ itemUsabilityOverride: (args) => {
+ const dataContext = args?.dataContext;
+ return !dataContext.completed;
+ },
},
{ option: 1, iconCssClass: 'mdi mdi-star-outline yellow', title: 'Low' },
{ option: 2, iconCssClass: 'mdi mdi-star orange', title: 'Medium' },
{ option: 3, iconCssClass: 'mdi mdi-star red', title: 'High' },
- }]
+ ]
}
```
@@ -160,11 +259,11 @@ It works exactly like the rest of the library when `enableTranslate` is set, all
```ts
contextMenu: {
optionTitleKey: 'OPTIONS', // optionally pass a title to show over the Options
- optionItems: [{
+ optionItems: [
{ option: 1, titleKey: 'LOW', iconCssClass: 'mdi mdi-star-outline yellow' },
{ option: 2, titleKey: 'MEDIUM', iconCssClass: 'mdi mdi-star orange' },
{ option: 3, titleKey: 'HIGH', iconCssClass: 'mdi mdi-star red' },
- }]
+ ]
}
```
@@ -184,25 +283,25 @@ Another set of possible Commands would be related to Grouping, so if you are usi
All of these internal commands, you can choose to hide them and/or change their icons, the default global options are the following and you can change any of them.
```ts
contextMenu: {
- autoAdjustDrop: true,
- autoAlignSide: true,
- hideCloseButton: true,
- hideClearAllGrouping: false,
- hideCollapseAllGroups: false,
- hideCommandSection: false,
- hideCopyCellValueCommand: false,
- hideExpandAllGroups: false,
- hideExportCsvCommand: false,
- hideExportExcelCommand: false,
- hideExportTextDelimitedCommand: true,
- hideMenuOnScroll: true,
- hideOptionSection: false,
- iconCopyCellValueCommand: 'mdi mdi-content-copy',
- iconExportCsvCommand: 'mdi mdi-download',
- iconExportExcelCommand: 'mdi mdi-file-excel-outline text-success',
- iconExportTextDelimitedCommand: 'mdi mdi-download',
- width: 200,
- },
+ autoAdjustDrop: true,
+ autoAlignSide: true,
+ hideCloseButton: true,
+ hideClearAllGrouping: false,
+ hideCollapseAllGroups: false,
+ hideCommandSection: false,
+ hideCopyCellValueCommand: false,
+ hideExpandAllGroups: false,
+ hideExportCsvCommand: false,
+ hideExportExcelCommand: false,
+ hideExportTextDelimitedCommand: true,
+ hideMenuOnScroll: true,
+ hideOptionSection: false,
+ iconCopyCellValueCommand: 'mdi mdi-content-copy',
+ iconExportCsvCommand: 'mdi mdi-download',
+ iconExportExcelCommand: 'mdi mdi-file-excel-outline text-success',
+ iconExportTextDelimitedCommand: 'mdi mdi-download',
+ width: 200,
+},
```
### How to Disable the Context Menu?
diff --git a/docs/grid-functionalities/grid-menu.md b/docs/grid-functionalities/grid-menu.md
index e69d0c03ee..2e1befef10 100644
--- a/docs/grid-functionalities/grid-menu.md
+++ b/docs/grid-functionalities/grid-menu.md
@@ -21,6 +21,8 @@ The Grid Menu also comes, by default, with a list of built-in custom commands (a
- _Refresh Dataset_, only shown when using Backend Service API (you can hide it with `hideRefreshDatasetCommand: true`)
This section is called Custom Commands because you can also customize this section with your own commands. To do that, you need to fill in 2 properties (an array of `commandItems` and define `onGridMenuCommand` callback) in your Grid Options. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
+
+#### Using Static Command Items
```ts
this.gridOptions = {
enableAutoResize: true,
@@ -73,6 +75,89 @@ this.gridOptions = {
};
```
+#### Advanced: Dynamic Command List Builder
+For more advanced use cases where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list. This function is executed **after** `commandItems` is processed and is the **last call before rendering** the menu in the DOM.
+
+**When to use `commandListBuilder`:**
+- You want to append/prepend items to the built-in commands
+- You need to filter commands based on runtime conditions
+- You want to sort or reorder commands dynamically
+- You need access to both built-in and custom commands to manipulate the final list
+
+**Note:** You would typically use `commandListBuilder` **instead of** `commandItems` (not both), since the builder gives you full control over the final command list.
+
+```ts
+gridOptions: {
+ gridMenu: {
+ // Build the command list dynamically
+ commandListBuilder: (builtInItems) => {
+ // Example 1: Append custom commands to built-in ones
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'help',
+ title: 'Help',
+ iconCssClass: 'mdi mdi-help-circle',
+ positionOrder: 99,
+ action: () => window.open('https://example.com/help', '_blank')
+ },
+ ];
+ },
+ onCommand: (e, args) => {
+ if (args.command === 'help') {
+ // command handled via action callback above
+ }
+ }
+ }
+}
+```
+
+**Example: Filter commands based on user permissions**
+```ts
+gridOptions: {
+ gridMenu: {
+ commandListBuilder: (builtInItems) => {
+ // Remove export commands if user doesn't have export permission
+ if (!this.userHasExportPermission) {
+ return builtInItems.filter(item =>
+ item !== 'divider' &&
+ !item.command?.includes('export')
+ );
+ }
+ return builtInItems;
+ }
+ }
+}
+```
+
+**Example: Reorder and customize the command list**
+```ts
+gridOptions: {
+ gridMenu: {
+ commandListBuilder: (builtInItems) => {
+ // Add custom commands at the beginning
+ const customCommands = [
+ {
+ command: 'refresh-cache',
+ title: 'Refresh Cache',
+ iconCssClass: 'mdi mdi-cached',
+ action: () => this.refreshCache()
+ },
+ 'divider'
+ ];
+
+ // Sort built-in items by title
+ const sortedBuiltIn = builtInItems
+ .filter(item => item !== 'divider')
+ .sort((a, b) => (a.title || '').localeCompare(b.title || ''));
+
+ return [...customCommands, ...sortedBuiltIn];
+ }
+ }
+}
+```
+
#### Events
There are multiple events/callback hooks which are accessible from the Grid Options
- `onBeforeMenuShow`
@@ -105,6 +190,11 @@ gridMenu: {
For more info on all the available properties of the custom commands, you can read refer to the doc written in the Grid Menu [implementation](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/extensions/slickGridMenu.ts) itself.
+### Custom Menu Item Rendering
+To customize the appearance of menu items with custom HTML, badges, icons, or interactive elements, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content.
+
### How to change an icon of all default commands?
You can change any of the default command icon(s) by changing the `icon[X-command]`, for example, see below for the defaults.
```ts
diff --git a/docs/grid-functionalities/header-menu-header-buttons.md b/docs/grid-functionalities/header-menu-header-buttons.md
index 555e7ca51c..861f69b24e 100644
--- a/docs/grid-functionalities/header-menu-header-buttons.md
+++ b/docs/grid-functionalities/header-menu-header-buttons.md
@@ -20,7 +20,10 @@ The Header Menu also comes, by default, with a list of built-in custom commands
- Sort Descending (you can hide it with `hideSortCommands: true`)
- Hide Column (you can hide it with `hideColumnHideCommand: true`)
-This section is called Custom Commands because you can also customize this section with your own commands. To do that, you need to fill in 2 properties (an array of `headerMenuItems` that will go under each column definition and define `onCommand` callbacks) in your Grid Options. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
+This section is called Custom Commands because you can also customize this section with your own commands. You can do this in two ways: using static command items or using a dynamic command list builder.
+
+#### Static Command Items
+To add static commands, fill in an array of items in your column definition's `header.menu.commandItems`. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
```ts
this.gridOptions = {
enableAutoResize: true,
@@ -51,6 +54,99 @@ this.gridOptions = {
}
};
```
+#### Dynamic Command List Builder
+For advanced use cases where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list. This is executed **after** `commandItems` and is the **last call before rendering**.
+
+```ts
+this.columnDefinitions = [
+ {
+ id: 'title',
+ name: 'Title',
+ field: 'title',
+ header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ // Append custom commands to the built-in sort/hide commands
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'freeze-column',
+ title: 'Freeze Column',
+ iconCssClass: 'mdi mdi-pin',
+ action: (e, args) => {
+ // Implement column freezing
+ console.log('Freeze column:', args.column.name);
+ }
+ }
+ ];
+ }
+ }
+ }
+ }
+];
+```
+
+**Example: Conditional commands based on column type**
+```ts
+header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ const column = this; // column context
+
+ // Add filtering option only for filterable columns
+ const extraCommands = [];
+ if (column.filterable !== false) {
+ extraCommands.push({
+ command: 'clear-filter',
+ title: 'Clear Filter',
+ iconCssClass: 'mdi mdi-filter-remove',
+ action: (e, args) => {
+ this.filterService.clearFilterByColumnId(args.column.id);
+ }
+ });
+ }
+
+ return [...builtInItems, ...extraCommands];
+ }
+ }
+}
+```
+
+**Example: Remove sort commands, keep only custom ones**
+```ts
+header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ // Filter out sort commands
+ const filtered = builtInItems.filter(item =>
+ item !== 'divider' &&
+ !item.command?.includes('sort')
+ );
+
+ // Add custom commands
+ return [
+ ...filtered,
+ {
+ command: 'custom-action',
+ title: 'Custom Action',
+ iconCssClass: 'mdi mdi-star',
+ action: (e, args) => alert('Custom: ' + args.column.name)
+ }
+ ];
+ }
+ }
+}
+```
+
+**When to use `commandListBuilder`:**
+- You want to append/prepend items to built-in commands
+- You need to filter or modify commands based on column properties
+- You want to customize the command list per column dynamically
+- You need full control over the final command list
+
+**Note:** Use `commandListBuilder` **instead of** `commandItems`, not both together.
+
#### Callback Hooks
There are 2 callback hooks which are accessible in the Grid Options
- `onBeforeMenuShow`
@@ -58,6 +154,11 @@ There are 2 callback hooks which are accessible in the Grid Options
For more info on all the available properties of the custom commands, you can read refer to the doc written in the Grid Menu [implementation](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/extensions/slickHeaderButtons.ts) itself.
+### Custom Menu Item Rendering
+To customize the appearance of menu items with custom HTML, badges, icons, or interactive elements, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content.
+
### How to change icon(s) of the default commands?
You can change any of the default command icon(s) by changing the `icon[X-command]`, for example, see below for the defaults.
```ts
diff --git a/docs/menu-slots.md b/docs/menu-slots.md
new file mode 100644
index 0000000000..14b9f9bb47
--- /dev/null
+++ b/docs/menu-slots.md
@@ -0,0 +1,524 @@
+## Custom Menu Slots - Rendering
+
+All menu plugins (Header Menu, Cell Menu, Context Menu, Grid Menu) support **cross-framework compatible slot rendering** for custom content injection in menu items. This is achieved through the `slotRenderer` callback at the item level combined with an optional `defaultMenuItemRenderer` at the menu level.
+
+> **Note:** This documentation covers **how menu items are rendered** (visual presentation). If you need to **dynamically modify which commands appear** in the menu (filtering, sorting, adding/removing items), see the `commandListBuilder` callback documented in [Grid Menu](grid-functionalities/grid-menu.md), [Context Menu](grid-functionalities/context-menu.md), or [Header Menu](grid-functionalities/header-menu-header-buttons.md).
+
+### TypeScript Tip: Type Inference with commandListBuilder
+
+When using `commandListBuilder` to add custom menu items with slotRenderer callbacks, **cast the return value to the appropriate type** to enable proper type parameters in callbacks:
+
+```typescript
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ return [
+ ...builtInItems,
+ {
+ command: 'custom-action',
+ title: 'My Action',
+ slotRenderer: (cmdItem, args) => `
${cmdItem.title}
`,
+ }
+ ] as Array;
+ }
+}
+```
+
+Alternatively, if you only have built-in items and dividers, you can use the simpler cast:
+
+```typescript
+return [...] as Array;
+```
+
+### Core Concept
+
+Each menu item can define a `slotRenderer` callback function that receives the item and args, and returns either an HTML string or an HTMLElement. This single API works uniformly across all menu plugins.
+
+### Slot Renderer Callback
+
+```typescript
+slotRenderer?: (cmdItem: MenuItem, args: MenuCallbackArgs, event?: Event) => string | HTMLElement
+```
+
+- **cmdItem** - The menu cmdItem object containing command, title, iconCssClass, etc.
+- **args** - The callback args providing access to grid, column, dataContext, and other context
+- **event** - Optional DOM event passed during click handling (allows `stopPropagation()`)
+
+### Basic Example - HTML String Rendering
+
+```typescript
+const menuItem = {
+ command: 'custom-command',
+ title: 'Custom Action',
+ iconCssClass: 'mdi mdi-star',
+ // Return custom HTML string for the entire menu item
+ slotRenderer: () => `
+
+
+ Custom Action
+ NEW
+
+ `
+};
+```
+
+### Advanced Example - HTMLElement Objects
+
+```typescript
+// Create custom element with full DOM control
+const menuItem = {
+ command: 'notifications',
+ title: 'Notifications',
+ // Return HTMLElement for more control and event listeners
+ slotRenderer: (cmdItem, args) => {
+ const container = document.createElement('div');
+ container.style.display = 'flex';
+ container.style.alignItems = 'center';
+
+ const icon = document.createElement('i');
+ icon.className = 'mdi mdi-bell';
+ icon.style.marginRight = '8px';
+
+ const text = document.createElement('span');
+ text.textContent = cmdItem.title;
+
+ const badge = document.createElement('span');
+ badge.className = 'badge';
+ badge.textContent = '5';
+ badge.style.marginLeft = 'auto';
+
+ container.appendChild(icon);
+ container.appendChild(text);
+ container.appendChild(badge);
+
+ return container;
+ }
+};
+```
+
+### Default Menu-Level Renderer
+
+Set a `defaultMenuItemRenderer` at the menu option level to apply to all items (unless overridden by individual `slotRenderer`):
+
+```typescript
+const menuOption = {
+ // Apply this renderer to all menu items (can be overridden per item)
+ defaultMenuItemRenderer: (cmdItem, args) => {
+ return `
+
';
+ }
+ }
+}
+```
diff --git a/docs/migrations/migration-to-10.x.md b/docs/migrations/migration-to-10.x.md
index 91b8aa2655..0497ce0e33 100644
--- a/docs/migrations/migration-to-10.x.md
+++ b/docs/migrations/migration-to-10.x.md
@@ -1,4 +1,4 @@
-## Cleaner Code / Smaller Code ⚡
+## Simplification and Modernization ⚡
One of the biggest change of this release is to hide columns by using the `hidden` column property (now used by Column Picker, Grid Menu, etc...). Previously we were removing columns from the original columns array and we then called `setColumns()` to update the grid, but this meant that we had to keep references for all visible and non-visible columns. With this new release we now keep the full columns array at all time and instead we just change column(s) visibility via their `hidden` column properties by using `grid.updateColumnById('id', { hidden: true })` and finally we update the grid via `grid.updateColumns()`. What I'm trying to emphasis is that you should really stop using `grid.setColumns()` in v10+, and if you want to hide some columns when declaring the columns, then just update their `hidden` properties, see more details below...
@@ -64,11 +64,30 @@ gridOptions = {
};
```
-### External Resources are now auto-enabled
-
This change does not require any code update from the end user, but it is a change that you should probably be aware of nonetheless. The reason I decided to implement this is because I often forget myself to enable the associated flag and typically if you wanted to load the resource, then it's most probably because you also want it enabled. So for example, if your register `ExcelExportService` then the library will now auto-enable the resource with its associated flag (which in this case is `enableExcelExport:true`)... unless you already disabled the flag (or enabled) yourself, if so then the internal assignment will simply be skipped and yours will prevail. Also just to be clear, the list of auto-enabled external resources is rather small, it will auto-enable the following resources:
(ExcelExportService, PdfExportService, TextExportService, CompositeEditorComponent and RowDetailView).
+### Menu with Commands
+
+All menu plugins (Cell Menu, Context Menu, Header Menu and Grid Menu) now have a new `commandListBuilder: (items) => items` which now allow you to filter/sort and maybe override built-in commands. With this new feature in place, I'm deprecating all `hide...` properties and also `positionOrder` since you can now do that with the builder. You could also use the `hideCommands` which accepts an array of built-in command names. This well remove huge amount of `hide...` properties (over 30) that keeps increasing anytime a new built-in command gets added (in other words, this will simplify maintenance for both you and me).This well remove huge amount of `hide...` properties (over 30) that keeps increasing anytime a new built-in command gets added (in other words, this will simplify maintenance for both you and me).
+
+These are currently just deprecations in v10.x but it's strongly recommended to start using the `commandListBuilder` and/or `hideCommands` and move away from the deprecated properties which will be removed in v11.x. For example if we want to hide some built-in commands:
+
+```diff
+gridOptions = {
+ gridMenu: {
+- hideExportCsvCommand: true,
+- hideTogglePreHeaderCommand: true,
+
+// via command name(s)
++ hideCommands: ['export-csv', 'toggle-preheader'],
+
+// or via builder
++ commandListBuilder: (cmdItems) => [...cmdItems.filter(x => x !== 'divider' && x.command !== 'export-csv' && x.command !== 'toggle-preheader')]
+ }
+}
+```
+
---
{% hint style="note" %}
diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example-header-menu-slots.html b/examples/vite-demo-vanilla-bundle/src/examples/example-header-menu-slots.html
new file mode 100644
index 0000000000..8c5544182c
--- /dev/null
+++ b/examples/vite-demo-vanilla-bundle/src/examples/example-header-menu-slots.html
@@ -0,0 +1,227 @@
+
+
+
+
+
+ Header Menu with Slots - Example
+
+
+
+
Header Menu with Slots - Example
+
This example demonstrates the new slot functionality in Header Menu items.
+
+
+
+
+
Slot Examples Demonstrated:
+
+
Resize by Content - Badge using slotContentAfter (HTML string)
+
Sort Commands - Keyboard shortcuts using slotContentAfter
+
Clear Sort - Status indicator using slotIconAfter (HTMLElement)
+
Custom Action - Custom renderer with complete control
+
+
+
+
+
+
diff --git a/frameworks-plugins/angular-row-detail-plugin/tsconfig.json b/frameworks-plugins/angular-row-detail-plugin/tsconfig.json
index 84056217cc..5b4bd341dd 100644
--- a/frameworks-plugins/angular-row-detail-plugin/tsconfig.json
+++ b/frameworks-plugins/angular-row-detail-plugin/tsconfig.json
@@ -21,7 +21,6 @@
"skipLibCheck": true,
"sourceMap": true,
"newLine": "lf",
- "downlevelIteration": true,
"outDir": "dist"
},
"include": ["src"],
diff --git a/frameworks-plugins/aurelia-row-detail-plugin/tsconfig.json b/frameworks-plugins/aurelia-row-detail-plugin/tsconfig.json
index 82191a3a37..201c1b1dc0 100644
--- a/frameworks-plugins/aurelia-row-detail-plugin/tsconfig.json
+++ b/frameworks-plugins/aurelia-row-detail-plugin/tsconfig.json
@@ -21,7 +21,6 @@
"skipLibCheck": true,
"sourceMap": true,
"newLine": "lf",
- "downlevelIteration": true,
"outDir": "dist"
},
"include": ["src"],
diff --git a/frameworks-plugins/react-row-detail-plugin/tsconfig.json b/frameworks-plugins/react-row-detail-plugin/tsconfig.json
index 82191a3a37..201c1b1dc0 100644
--- a/frameworks-plugins/react-row-detail-plugin/tsconfig.json
+++ b/frameworks-plugins/react-row-detail-plugin/tsconfig.json
@@ -21,7 +21,6 @@
"skipLibCheck": true,
"sourceMap": true,
"newLine": "lf",
- "downlevelIteration": true,
"outDir": "dist"
},
"include": ["src"],
diff --git a/frameworks-plugins/vue-row-detail-plugin/tsconfig.json b/frameworks-plugins/vue-row-detail-plugin/tsconfig.json
index 82191a3a37..201c1b1dc0 100644
--- a/frameworks-plugins/vue-row-detail-plugin/tsconfig.json
+++ b/frameworks-plugins/vue-row-detail-plugin/tsconfig.json
@@ -21,7 +21,6 @@
"skipLibCheck": true,
"sourceMap": true,
"newLine": "lf",
- "downlevelIteration": true,
"outDir": "dist"
},
"include": ["src"],
diff --git a/frameworks/angular-slickgrid/docs/TOC.md b/frameworks/angular-slickgrid/docs/TOC.md
index f92a407057..b89496d92c 100644
--- a/frameworks/angular-slickgrid/docs/TOC.md
+++ b/frameworks/angular-slickgrid/docs/TOC.md
@@ -53,8 +53,8 @@
* [Add, Update or Highlight a Datagrid Item](grid-functionalities/add-update-highlight.md)
* [Dynamically Add CSS Classes to Item Rows](grid-functionalities/dynamic-item-metadata.md)
* [Column & Row Spanning](grid-functionalities/column-row-spanning.md)
-* [Context Menu](grid-functionalities/Context-Menu.md)
-* [Custom Footer](grid-functionalities/Custom-Footer.md)
+* [Context Menu](grid-functionalities/context-menu.md)
+* [Custom Footer](grid-functionalities/custom-footer.md)
* [Excel Copy Buffer Plugin](grid-functionalities/excel-copy-buffer.md)
* [Export to Excel](grid-functionalities/Export-to-Excel.md)
* [Export to PDF](grid-functionalities/Export-to-PDF.md)
diff --git a/frameworks/angular-slickgrid/docs/column-functionalities/cell-menu.md b/frameworks/angular-slickgrid/docs/column-functionalities/cell-menu.md
index ded4c4aada..cd8417ff8e 100644
--- a/frameworks/angular-slickgrid/docs/column-functionalities/cell-menu.md
+++ b/frameworks/angular-slickgrid/docs/column-functionalities/cell-menu.md
@@ -94,7 +94,8 @@ So if you decide to use the `action` callback, then your code would look like th
##### with `action` callback
```ts
this.columnDefinitions = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
commandItems: [
{ command: 'command1', title: 'Command 1', action: (e, args) => console.log(args) },
@@ -111,7 +112,8 @@ The `onCommand` (or `onOptionSelected`) **must** be defined in the Grid Options
```ts
this.columnDefinitions = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
commandItems: [
{ command: 'command1', title: 'Command 1' },
@@ -140,6 +142,11 @@ this.gridOptions = {
};
```
+### Custom Menu Item Rendering
+For advanced customization of menu item appearance, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks to create custom HTML or HTMLElement content for your menu items. This allows you to add badges, keyboard shortcuts, status indicators, and more.
+
+See [Custom Menu Slots](../../menu-slots.md) for detailed examples and best practices on rendering custom menu item content across all menu types.
+
### Override Callback Methods
What if you want to dynamically disable or hide a Command/Option or even disable the entire menu in certain circumstances? For these cases, you would use the override callback methods, the method must return a `boolean`. The list of override available are the following
- `menuUsabilityOverride` returning false would make the Cell Menu unavailable to the user
@@ -150,12 +157,10 @@ What if you want to dynamically disable or hide a Command/Option or even disable
For example, say we want the Cell Menu to only be available on the first 20 rows of the grid, we could use the override this way
```ts
this.columnDefinitions = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
- menuUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
- return (dataContext.id < 21); // say we want to display the menu only from Task 0 to 20
- },
+ menuUsabilityOverride: (args) => args?.dataContext.id < 21, // say we want to display the menu only from Task 0 to 20
}
}
];
@@ -164,15 +169,17 @@ this.columnDefinitions = [
To give another example, with Options this time, we could say that we enable the `n/a` option only when the row is Completed. So we could do it this way
```ts
this.columnDefinitions = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
optionItems: [
- {
- option: 0, title: 'n/a', textCssClass: 'italic',
- // only enable this option when the task is Not Completed
- itemUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
- return !dataContext.completed;
+ {
+ option: 0, title: 'n/a', textCssClass: 'italic',
+ // only enable this option when the task is Not Completed
+ itemUsabilityOverride: (args) => {
+ const dataContext = args?.dataContext;
+ return !dataContext.completed;
+ },
},
{ option: 1, iconCssClass: 'mdi mdi-star-outline yellow', title: 'Low' },
{ option: 2, iconCssClass: 'mdi mdi-star orange', title: 'Medium' },
@@ -187,7 +194,8 @@ this.columnDefinitions = [
It works exactly like the rest of the library when `enableTranslate` is set, all we have to do is to provide translations with the `Key` suffix, so for example without translations, we would use `title` and that would become `titleKey` with translations, that;'s easy enough. So for example, a list of Options could be defined as follow:
```ts
this.columnDefinitions = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
optionTitleKey: 'OPTIONS', // optionally pass a title to show over the Options
optionItems: [
diff --git a/frameworks/angular-slickgrid/docs/column-functionalities/editors.md b/frameworks/angular-slickgrid/docs/column-functionalities/editors.md
index 5ab1f98c16..24f2903504 100644
--- a/frameworks/angular-slickgrid/docs/column-functionalities/editors.md
+++ b/frameworks/angular-slickgrid/docs/column-functionalities/editors.md
@@ -282,7 +282,7 @@ this.columnDefinitions = [
collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }],
model: Editors.singleSelect,
elementOptions: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
}
@@ -501,10 +501,10 @@ So if we take all of these informations and we want to create our own Custom Edi
```ts
const myCustomTitleValidator: EditorValidator = (value: any, args: EditorArgs) => {
// you can get the Editor Args which can be helpful, e.g. we can get the Translate Service from it
- const grid = args && args.grid;
+ const grid = args.grid;
const columnDef = args.column;
const dataContext = args.item;
- const gridOptions = (grid && grid.getOptions) ? grid.getOptions() : {};
+ const gridOptions = grid.getOptions() : {};
const i18n = gridOptions.i18n;
if (value == null || value === undefined || !value.length) {
diff --git a/frameworks/angular-slickgrid/docs/column-functionalities/editors/select-dropdown-editor.md b/frameworks/angular-slickgrid/docs/column-functionalities/editors/select-dropdown-editor.md
index 9e791eaad1..911884149c 100644
--- a/frameworks/angular-slickgrid/docs/column-functionalities/editors/select-dropdown-editor.md
+++ b/frameworks/angular-slickgrid/docs/column-functionalities/editors/select-dropdown-editor.md
@@ -6,11 +6,11 @@
- [Collection Label Prefix/Suffix](#collection-label-prefixsuffix)
- [Collection Label Render HTML](#collection-label-render-html)
- [Collection Change Watch](#collection-watch)
- - [`multiple-select.js` Options](#multiple-selectjs-options)
+ - [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- See the [Editors - Wiki](../Editors.md) for more general info about Editors (validators, event handlers, ...)
## Select Editors
-The library ships with two select editors: `singleSelectEditor` and the `multipleSelectEditor`. Both support the [multiple-select](https://github.com/ghiscoding/multiple-select-adapted/blob/master/src/multiple-select.js) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/collectionFormatter.ts).
+The library ships with two select editors: `singleSelectEditor` and the `multipleSelectEditor`. Both support the [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/collectionFormatter.ts).
We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla).
@@ -37,7 +37,7 @@ this.columnDefinitions = [
```
### Editor Options (`MultipleSelectOption` interface)
-All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select.js` library.
+All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select-vanilla` library.
```ts
editor: {
@@ -190,8 +190,8 @@ this.columnDefinitions = [
];
```
-### `multiple-select.js` Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+### `multiple-select-vanilla` Options
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/lib` folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -217,7 +217,7 @@ this.columnDefinitions = [
collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }],
model: Editors.singleSelect,
elementOptions: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
}
diff --git a/frameworks/angular-slickgrid/docs/column-functionalities/filters/select-filter.md b/frameworks/angular-slickgrid/docs/column-functionalities/filters/select-filter.md
index fe7b55f7d7..f135a24310 100644
--- a/frameworks/angular-slickgrid/docs/column-functionalities/filters/select-filter.md
+++ b/frameworks/angular-slickgrid/docs/column-functionalities/filters/select-filter.md
@@ -16,7 +16,7 @@
- [Collection Async Load](#collection-async-load)
- [Collection Lazy Load](#collection-lazy-load)
- [Collection Watch](#collection-watch)
-- [`multiple-select.js` Options](#multiple-selectjs-options)
+- [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- [Filter Options (`MultipleSelectOption` interface)](#filter-options-multipleselectoption-interface)
- [Display shorter selected label text](#display-shorter-selected-label-text)
- [Query against a different field](#query-against-another-field-property)
@@ -33,7 +33,7 @@ Multiple Select (dropdown) filter is useful when we want to filter the grid 1 or
We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla).
#### Note
-For this filter to work you will need to add [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) to your project. This is a customized version of the original (thought all the original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options). Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/dist/lib` folder. This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original)
+For this filter to work you will need to add [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) to your project. This is a customized version of the original (thought all the original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options). Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/dist/lib` folder. This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original)
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
- `okButtonText` was also added for locale (i18n)
- `offsetLeft` option was added to make it possible to offset the dropdown. By default it is set to 0 and is aligned to the left of the select element. This option is particularly helpful when used as the last right column, not to fall off the screen.
@@ -576,7 +576,7 @@ this.columnDefinitions = [
```
### Filter Options (`MultipleSelectOption` interface)
-All the available options that can be provided as filter `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your filter `options` to that interface to make sure that you use only valid options of the `multiple-select.js` library.
+All the available options that can be provided as filter `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your filter `options` to that interface to make sure that you use only valid options of the `multiple-select-vanilla` library.
```ts
filter: {
@@ -599,8 +599,8 @@ this.gridOptions = {
}
```
-### Multiple-select.js Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your filter `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+### multiple-select-vanilla Options
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your filter `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit SlickGrid-Universal needs, which is why we are using a fork [ghiscoding/multiple-select-modified](https://github.com/ghiscoding/multiple-select-modified) folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -629,7 +629,7 @@ this.columnDefinitions = [
model: Filters.singleSelect,
// previously known as `filterOptions` for < 9.0
options: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
} as MultipleSelectOption
diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/Header-Menu-&-Header-Buttons.md b/frameworks/angular-slickgrid/docs/grid-functionalities/Header-Menu-&-Header-Buttons.md
index 7742b7653e..9129356b5f 100644
--- a/frameworks/angular-slickgrid/docs/grid-functionalities/Header-Menu-&-Header-Buttons.md
+++ b/frameworks/angular-slickgrid/docs/grid-functionalities/Header-Menu-&-Header-Buttons.md
@@ -20,7 +20,10 @@ The Header Menu also comes, by default, with a list of built-in custom commands
- Sort Descending (you can hide it with `hideSortCommands: true`)
- Hide Column (you can hide it with `hideColumnHideCommand: true`)
-This section is called Custom Commands because you can also customize this section with your own commands. To do that, you need to fill in 2 properties (an array of `headerMenuItems` that will go under each column definition and define `onCommand` callbacks) in your Grid Options. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
+This section is called Custom Commands because you can also customize this section with your own commands. You can do this in two ways: using static command items or using a dynamic command list builder.
+
+#### Static Command Items
+To add static commands, fill in an array of items in your column definition's `header.menu.commandItems`. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
```ts
this.gridOptions = {
enableAutoResize: true,
@@ -51,6 +54,100 @@ this.gridOptions = {
}
};
```
+
+#### Dynamic Command List Builder
+For advanced use cases where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list. This is executed **after** `commandItems` and is the **last call before rendering**.
+
+```ts
+this.columnDefinitions = [
+ {
+ id: 'title',
+ name: 'Title',
+ field: 'title',
+ header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ // Append custom commands to the built-in sort/hide commands
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'freeze-column',
+ title: 'Freeze Column',
+ iconCssClass: 'mdi mdi-pin',
+ action: (e, args) => {
+ // Implement column freezing
+ console.log('Freeze column:', args.column.name);
+ }
+ }
+ ];
+ }
+ }
+ }
+ }
+];
+```
+
+**Example: Conditional commands based on column type**
+```ts
+header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ const column = this; // column context
+
+ // Add filtering option only for filterable columns
+ const extraCommands = [];
+ if (column.filterable !== false) {
+ extraCommands.push({
+ command: 'clear-filter',
+ title: 'Clear Filter',
+ iconCssClass: 'mdi mdi-filter-remove',
+ action: (e, args) => {
+ this.filterService.clearFilterByColumnId(args.column.id);
+ }
+ });
+ }
+
+ return [...builtInItems, ...extraCommands];
+ }
+ }
+}
+```
+
+**Example: Remove sort commands, keep only custom ones**
+```ts
+header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ // Filter out sort commands
+ const filtered = builtInItems.filter(item =>
+ item !== 'divider' &&
+ !item.command?.includes('sort')
+ );
+
+ // Add custom commands
+ return [
+ ...filtered,
+ {
+ command: 'custom-action',
+ title: 'Custom Action',
+ iconCssClass: 'mdi mdi-star',
+ action: (e, args) => alert('Custom: ' + args.column.name)
+ }
+ ];
+ }
+ }
+}
+```
+
+**When to use `commandListBuilder`:**
+- You want to append/prepend items to built-in commands
+- You need to filter or modify commands based on column properties
+- You want to customize the command list per column dynamically
+- You need full control over the final command list
+
+**Note:** Use `commandListBuilder` **instead of** `commandItems`, not both together.
+
#### Callback Hooks
There are 2 callback hooks which are accessible in the Grid Options
- `onBeforeMenuShow`
@@ -58,6 +155,11 @@ There are 2 callback hooks which are accessible in the Grid Options
For more info on all the available properties of the custom commands, you can read refer to the doc written in the Grid Menu [implementation](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/extensions/slickHeaderButtons.ts) itself.
+### Custom Menu Item Rendering
+To customize the appearance of menu items with custom HTML, badges, icons, or interactive elements, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content.
+
### How to change icon(s) of the default commands?
You can change any of the default command icon(s) by changing the `icon[X-command]`, for example, see below for the defaults.
```ts
diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/composite-editor-modal.md b/frameworks/angular-slickgrid/docs/grid-functionalities/composite-editor-modal.md
index b9ea239db2..ce3c9cf392 100644
--- a/frameworks/angular-slickgrid/docs/grid-functionalities/composite-editor-modal.md
+++ b/frameworks/angular-slickgrid/docs/grid-functionalities/composite-editor-modal.md
@@ -476,7 +476,7 @@ export class GridExample {
// you can also change some editor options
// not all Editors supports this functionality, so far only these Editors are supported: AutoComplete, Date, Single/Multiple Select
if (columnDef.id === 'completed') {
- this.compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select.js, show filter in dropdown
+ this.compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select-vanilla, show filter in dropdown
this.compositeEditorInstance.changeFormEditorOption('product', 'minLength', 3); // autocomplete, change minLength char to type
this.compositeEditorInstance.changeFormEditorOption('finish', 'displayDateMin', 'today');
}
diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/Context-Menu.md b/frameworks/angular-slickgrid/docs/grid-functionalities/context-menu.md
similarity index 77%
rename from frameworks/angular-slickgrid/docs/grid-functionalities/Context-Menu.md
rename to frameworks/angular-slickgrid/docs/grid-functionalities/context-menu.md
index 4396e689ff..0a4c6cd168 100644
--- a/frameworks/angular-slickgrid/docs/grid-functionalities/Context-Menu.md
+++ b/frameworks/angular-slickgrid/docs/grid-functionalities/context-menu.md
@@ -23,7 +23,7 @@ This extensions is wrapped around the new SlickGrid Plugin **SlickContextMenu**
### Default Usage
Technically, the Context Menu is enabled by default (copy, export) and so you don't have anything to do to enjoy it (you could disable it at any time). However, if you want to customize the content of the Context Menu, then continue reading. You can customize the menu with 2 different lists, Commands and/or Options, they can be used separately or at the same time. Also note that even though the code shown below makes a separation between the Commands and Options, you can mix them in the same Context Menu.
-#### with Commands
+#### with Commands (Static)
```ts
this.gridOptions = {
@@ -63,6 +63,98 @@ this.gridOptions = {
};
```
+#### with Commands (Dynamic Builder)
+For advanced scenarios where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list.
+
+```ts
+this.gridOptions = {
+ enableContextMenu: true,
+ contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ // Example: Add custom commands after built-in ones
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'delete-row',
+ title: 'Delete Row',
+ iconCssClass: 'mdi mdi-delete text-danger',
+ action: (e, args) => {
+ if (confirm(`Delete row ${args.dataContext.id}?`)) {
+ this.gridService.deleteItem(args.dataContext);
+ }
+ }
+ },
+ {
+ command: 'duplicate-row',
+ title: 'Duplicate Row',
+ iconCssClass: 'mdi mdi-content-duplicate',
+ action: (e, args) => {
+ const newItem = { ...args.dataContext, id: this.generateNewId() };
+ this.gridService.addItem(newItem);
+ }
+ }
+ ];
+ },
+ onCommand: (e, args) => {
+ // Handle commands here if not using action callbacks
+ console.log('Command:', args.command);
+ }
+ }
+};
+```
+
+**Example: Filter commands based on row data**
+```ts
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ // You can't access row data here, but you can filter/modify built-in items
+ // Use itemUsabilityOverride or itemVisibilityOverride for row-specific logic
+
+ // Only show export commands
+ return builtInItems.filter(item =>
+ item === 'divider' || item.command?.includes('export')
+ );
+ }
+}
+```
+
+**Example: Sort and reorganize commands**
+```ts
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ const customFirst = [
+ {
+ command: 'edit',
+ title: 'Edit Row',
+ iconCssClass: 'mdi mdi-pencil',
+ positionOrder: 0
+ }
+ ];
+
+ // Sort built-in commands by positionOrder
+ const sorted = [...builtInItems].sort((a, b) => {
+ if (a === 'divider' || b === 'divider') return 0;
+ return (a.positionOrder || 50) - (b.positionOrder || 50);
+ });
+
+ return [...customFirst, 'divider', ...sorted];
+ }
+}
+```
+
+**When to use `commandListBuilder` vs `commandItems`:**
+- Use `commandItems` for static command lists
+- Use `commandListBuilder` when you need to:
+ - Append/prepend to built-in commands
+ - Filter or modify commands dynamically
+ - Sort or reorder the final command list
+ - Have full control over what gets rendered
+
+**Note:** Typically use `commandListBuilder` **instead of** `commandItems`, not both together.
+
+See the main [Custom Menu Slots](../menu-slots.md) documentation for detailed `commandListBuilder` examples.
+
#### with Options
That is when you want to define a list of Options (only 1 list) that the user can choose from and once is selected we would do something (for example change the value of a cell in the grid).
@@ -79,7 +171,7 @@ this.gridOptions = {
// subscribe to Context Menu onOptionSelected event (or use the "action" callback on each option)
onOptionSelected: (e, args) => {
// change Priority
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext && dataContext.hasOwnProperty('priority')) {
dataContext.priority = args.item.option;
this.sgb.gridService.updateItem(dataContext);
@@ -139,7 +231,7 @@ For example, say we want the Context Menu to only be available on the first 20 r
```ts
contextMenu: {
menuUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return (dataContext.id < 21); // say we want to display the menu only from Task 0 to 20
},
},
@@ -153,7 +245,7 @@ contextMenu: {
option: 0, title: 'n/a', textCssClass: 'italic',
// only enable this option when the task is Not Completed
itemUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
},
@@ -177,6 +269,11 @@ contextMenu: {
}
```
+### Custom Menu Item Rendering
+For advanced customization of menu item appearance, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks to create custom HTML or HTMLElement content for your menu items. This allows you to add badges, keyboard shortcuts, status indicators, and more.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content across all menu types.
+
### Show Menu only over Certain Columns
Say you want to show the Context Menu only when the user is over certain columns of the grid. For that, you could use the `commandShownOverColumnIds` (or `optionShownOverColumnIds`) array, by default these arrays are empty and when that is the case then the menu will be accessible from any columns. So if we want to have the Context Menu available only over the first 2 columns, we would have an array of those 2 column ids. For example, the following would show the Context Menu everywhere except the last 2 columns (priority, action) since they are not part of the array.
```ts
diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/Custom-Footer.md b/frameworks/angular-slickgrid/docs/grid-functionalities/custom-footer.md
similarity index 100%
rename from frameworks/angular-slickgrid/docs/grid-functionalities/Custom-Footer.md
rename to frameworks/angular-slickgrid/docs/grid-functionalities/custom-footer.md
diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/grid-menu.md b/frameworks/angular-slickgrid/docs/grid-functionalities/grid-menu.md
index a32879fa12..75ee618cde 100644
--- a/frameworks/angular-slickgrid/docs/grid-functionalities/grid-menu.md
+++ b/frameworks/angular-slickgrid/docs/grid-functionalities/grid-menu.md
@@ -74,6 +74,11 @@ this.gridOptions = {
};
```
+#### Advanced: Dynamic Command List Builder
+For more advanced use cases where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list.
+
+See the main [Custom Menu Slots](../menu-slots.md) documentation for detailed `commandListBuilder` examples.
+
#### Events
There are multiple events/callback hooks which are accessible from the Grid Options
@@ -107,6 +112,11 @@ gridMenu: {
For more info on all the available properties of the custom commands, you can read refer to the doc written in the Grid Menu [implementation](https://github.com/6pac/SlickGrid/blob/master/controls/slick.gridmenu.js) itself.
+### Custom Menu Item Rendering
+To customize the appearance of menu items with custom HTML, badges, icons, or interactive elements, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content.
+
### How to change an icon of all default commands?
You can change any of the default command icon(s) by changing the `icon[Command]`, for example, see below for the defaults.
diff --git a/frameworks/angular-slickgrid/docs/menu-slots.md b/frameworks/angular-slickgrid/docs/menu-slots.md
new file mode 100644
index 0000000000..14b9f9bb47
--- /dev/null
+++ b/frameworks/angular-slickgrid/docs/menu-slots.md
@@ -0,0 +1,524 @@
+## Custom Menu Slots - Rendering
+
+All menu plugins (Header Menu, Cell Menu, Context Menu, Grid Menu) support **cross-framework compatible slot rendering** for custom content injection in menu items. This is achieved through the `slotRenderer` callback at the item level combined with an optional `defaultMenuItemRenderer` at the menu level.
+
+> **Note:** This documentation covers **how menu items are rendered** (visual presentation). If you need to **dynamically modify which commands appear** in the menu (filtering, sorting, adding/removing items), see the `commandListBuilder` callback documented in [Grid Menu](grid-functionalities/grid-menu.md), [Context Menu](grid-functionalities/context-menu.md), or [Header Menu](grid-functionalities/header-menu-header-buttons.md).
+
+### TypeScript Tip: Type Inference with commandListBuilder
+
+When using `commandListBuilder` to add custom menu items with slotRenderer callbacks, **cast the return value to the appropriate type** to enable proper type parameters in callbacks:
+
+```typescript
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ return [
+ ...builtInItems,
+ {
+ command: 'custom-action',
+ title: 'My Action',
+ slotRenderer: (cmdItem, args) => `
${cmdItem.title}
`,
+ }
+ ] as Array;
+ }
+}
+```
+
+Alternatively, if you only have built-in items and dividers, you can use the simpler cast:
+
+```typescript
+return [...] as Array;
+```
+
+### Core Concept
+
+Each menu item can define a `slotRenderer` callback function that receives the item and args, and returns either an HTML string or an HTMLElement. This single API works uniformly across all menu plugins.
+
+### Slot Renderer Callback
+
+```typescript
+slotRenderer?: (cmdItem: MenuItem, args: MenuCallbackArgs, event?: Event) => string | HTMLElement
+```
+
+- **cmdItem** - The menu cmdItem object containing command, title, iconCssClass, etc.
+- **args** - The callback args providing access to grid, column, dataContext, and other context
+- **event** - Optional DOM event passed during click handling (allows `stopPropagation()`)
+
+### Basic Example - HTML String Rendering
+
+```typescript
+const menuItem = {
+ command: 'custom-command',
+ title: 'Custom Action',
+ iconCssClass: 'mdi mdi-star',
+ // Return custom HTML string for the entire menu item
+ slotRenderer: () => `
+
+
+ Custom Action
+ NEW
+
+ `
+};
+```
+
+### Advanced Example - HTMLElement Objects
+
+```typescript
+// Create custom element with full DOM control
+const menuItem = {
+ command: 'notifications',
+ title: 'Notifications',
+ // Return HTMLElement for more control and event listeners
+ slotRenderer: (cmdItem, args) => {
+ const container = document.createElement('div');
+ container.style.display = 'flex';
+ container.style.alignItems = 'center';
+
+ const icon = document.createElement('i');
+ icon.className = 'mdi mdi-bell';
+ icon.style.marginRight = '8px';
+
+ const text = document.createElement('span');
+ text.textContent = cmdItem.title;
+
+ const badge = document.createElement('span');
+ badge.className = 'badge';
+ badge.textContent = '5';
+ badge.style.marginLeft = 'auto';
+
+ container.appendChild(icon);
+ container.appendChild(text);
+ container.appendChild(badge);
+
+ return container;
+ }
+};
+```
+
+### Default Menu-Level Renderer
+
+Set a `defaultMenuItemRenderer` at the menu option level to apply to all items (unless overridden by individual `slotRenderer`):
+
+```typescript
+const menuOption = {
+ // Apply this renderer to all menu items (can be overridden per item)
+ defaultMenuItemRenderer: (cmdItem, args) => {
+ return `
+
';
+ }
+ }
+}
+```
diff --git a/frameworks/angular-slickgrid/docs/migrations/migration-to-10.x.md b/frameworks/angular-slickgrid/docs/migrations/migration-to-10.x.md
index 48e583ee65..175159a65c 100644
--- a/frameworks/angular-slickgrid/docs/migrations/migration-to-10.x.md
+++ b/frameworks/angular-slickgrid/docs/migrations/migration-to-10.x.md
@@ -1,4 +1,4 @@
-## Cleaner Code / Smaller Code ⚡
+## Simplification and Modernization ⚡
One of the biggest change of this release is to hide columns by using the `hidden` column property (now used by Column Picker, Grid Menu, etc...). Previously we were removing columns from the original columns array and we then called `setColumns()` to update the grid, but this meant that we had to keep references for all visible and non-visible columns. With this new release we now keep the full columns array at all time and instead we just change column(s) visibility via their `hidden` column properties by using `grid.updateColumnById('id', { hidden: true })` and finally we update the grid via `grid.updateColumns()`. What I'm trying to emphasis is that you should really stop using `grid.setColumns()` in v10+, and if you want to hide some columns when declaring the columns, then just update their `hidden` properties, see more details below...
@@ -89,11 +89,30 @@ gridOptions = {
};
```
-### External Resources are now auto-enabled
-
This change does not require any code update from the end user, but it is a change that you should probably be aware of nonetheless. The reason I decided to implement this is because I often forget myself to enable the associated flag and typically if you wanted to load the resource, then it's most probably because you also want it enabled. So for example, if your register `ExcelExportService` then the library will now auto-enable the resource with its associated flag (which in this case is `enableExcelExport:true`)... unless you already disabled the flag (or enabled) yourself, if so then the internal assignment will simply be skipped and yours will prevail. Also just to be clear, the list of auto-enabled external resources is rather small, it will auto-enable the following resources:
(ExcelExportService, PdfExportService, TextExportService, CompositeEditorComponent and RowDetailView).
+### Menu with Commands
+
+All menu plugins (Cell Menu, Context Menu, Header Menu and Grid Menu) now have a new `commandListBuilder: (items) => items` which now allow you to filter/sort and maybe override built-in commands. With this new feature in place, I'm deprecating all `hide...` properties and also `positionOrder` since you can now do that with the builder. You could also use the `hideCommands` which accepts an array of built-in command names. This well remove huge amount of `hide...` properties (over 30) that keeps increasing anytime a new built-in command gets added (in other words, this will simplify maintenance for both you and me).
+
+These are currently just deprecations in v10.x but it's strongly recommended to start using the `commandListBuilder` and/or `hideCommands` and move away from the deprecated properties which will be removed in v11.x. For example if we want to hide some built-in commands:
+
+```diff
+gridOptions = {
+ gridMenu: {
+- hideExportCsvCommand: true,
+- hideTogglePreHeaderCommand: true,
+
+// via command name(s)
++ hideCommands: ['export-csv', 'toggle-preheader'],
+
+// or via builder
++ commandListBuilder: (cmdItems) => [...cmdItems.filter(x => x !== 'divider' && x.command !== 'export-csv' && x.command !== 'toggle-preheader')]
+ }
+}
+```
+
### `ngx-translate` v17.x now required
Because of the Angular v21 upgrade, the user (you) will also need to upgrade [`ngx-translate`](https://ngx-translate.org/) to its latest version 17.x.
diff --git a/frameworks/angular-slickgrid/src/demos/app-routing.module.ts b/frameworks/angular-slickgrid/src/demos/app-routing.module.ts
index 817be2f5b4..7b1d88e1bf 100644
--- a/frameworks/angular-slickgrid/src/demos/app-routing.module.ts
+++ b/frameworks/angular-slickgrid/src/demos/app-routing.module.ts
@@ -52,6 +52,7 @@ export const routes: Routes = [
{ path: 'example48', loadComponent: () => import('./examples/example48.component').then((m) => m.Example48Component) },
{ path: 'example49', loadComponent: () => import('./examples/example49.component').then((m) => m.Example49Component) },
{ path: 'example50', loadComponent: () => import('./examples/example50.component').then((m) => m.Example50Component) },
+ { path: 'example51', loadComponent: () => import('./examples/example51.component').then((m) => m.Example51Component) },
{ path: '', redirectTo: '/example34', pathMatch: 'full' },
{ path: '**', redirectTo: '/example34', pathMatch: 'full' },
];
diff --git a/frameworks/angular-slickgrid/src/demos/app.component.html b/frameworks/angular-slickgrid/src/demos/app.component.html
index 65cf5ad3ab..d952e76aba 100644
--- a/frameworks/angular-slickgrid/src/demos/app.component.html
+++ b/frameworks/angular-slickgrid/src/demos/app.component.html
@@ -190,6 +190,9 @@
+ Click on the menu buttons to see the new single slot functionality working across all menu types (Header Menu, Cell
+ Menu, Context Menu, Grid Menu):
+
+
+ Note: The demo focuses on the custom rendering capability via slotRenderer and
+ defaultMenuItemRenderer, which work across all menu plugins (SlickHeaderMenu, SlickCellMenu, SlickContextMenu,
+ SlickGridMenu). Also note that the keyboard shortcuts displayed in the menus (e.g., Alt+↑, F5) are for demo
+ purposes only and do not actually trigger any actions.
+
+
+ `,
+ action: () => alert('Export to CSV'),
+ },
+ {
+ command: 'refresh-data',
+ title: 'Refresh Data',
+ iconCssClass: 'mdi mdi-refresh',
+ // Demo: slotRenderer with keyboard shortcut
+ slotRenderer: (cmdItem) => {
+ // you can use `createDomElement()` from Slickgrid for easier DOM element creation
+ const menuItemElm = createDomElement('div', { className: 'menu-item' });
+ const iconElm = createDomElement('i', { className: `${cmdItem.iconCssClass} menu-item-icon` });
+ const menuItemLabelElm = createDomElement('span', { className: 'menu-item-label', textContent: cmdItem.title || '' });
+ const kbdElm = createDomElement('kbd', { className: 'key-hint', textContent: 'F5' });
+ menuItemElm.appendChild(iconElm);
+ menuItemElm.appendChild(menuItemLabelElm);
+ menuItemElm.appendChild(kbdElm);
+ return menuItemElm;
+ },
+ action: () => alert('Refresh data'),
+ },
+ ] as Array;
+ },
+ },
+
+ // tooltip plugin
+ externalResources: [new SlickCustomTooltip()],
+ customTooltip: {
+ observeAllTooltips: true,
+ },
+ };
+ }
+
+ clearGrouping() {
+ this.angularGrid?.dataView?.setGrouping([]);
+ }
+
+ collapseAllGroups() {
+ this.angularGrid?.dataView?.collapseAllGroups();
+ }
+
+ expandAllGroups() {
+ this.angularGrid?.dataView?.expandAllGroups();
+ }
+
+ groupByDuration() {
+ // you need to manually add the sort icon(s) in UI
+ this.angularGrid?.slickGrid?.setSortColumns([{ columnId: 'duration', sortAsc: true }]);
+ this.angularGrid?.dataView?.setGrouping({
+ getter: 'duration',
+ formatter: (g) => `Duration: ${g.value} (${g.count} items)`,
+ comparer: (a, b) => SortComparers.numeric(a.value, b.value, SortDirectionNumber.asc),
+ aggregators: [new Aggregators.Avg('percentComplete'), new Aggregators.Sum('cost')],
+ aggregateCollapsed: false,
+ lazyTotalsCalculation: true,
+ } as Grouping);
+ this.angularGrid?.slickGrid?.invalidate(); // invalidate all rows and re-render
+ }
+
+ loadData(count: number): ReportItem[] {
+ const tmpData: ReportItem[] = [];
+ for (let i = 0; i < count; i++) {
+ const randomDuration = Math.round(Math.random() * 100);
+ const randomYear = 2000 + Math.floor(Math.random() * 10);
+ const randomMonth = Math.floor(Math.random() * 11);
+ const randomDay = Math.floor(Math.random() * 29);
+ const randomPercent = Math.round(Math.random() * 100);
+
+ tmpData[i] = {
+ id: i,
+ title: 'Task ' + i,
+ duration: randomDuration,
+ cost: Math.round(Math.random() * 10000) / 100,
+ percentComplete: randomPercent,
+ start: new Date(randomYear, randomMonth, randomDay),
+ finish: new Date(randomYear, randomMonth + 1, randomDay),
+ };
+ }
+ return tmpData;
+ }
+
+ toggleSubTitle() {
+ this.hideSubTitle = !this.hideSubTitle;
+ const action = this.hideSubTitle ? 'add' : 'remove';
+ document.querySelector('.subtitle')?.classList[action]('hidden');
+ this.angularGrid.resizerService.resizeGrid(0);
+ }
+}
diff --git a/frameworks/angular-slickgrid/test/cypress/e2e/example51.cy.ts b/frameworks/angular-slickgrid/test/cypress/e2e/example51.cy.ts
new file mode 100644
index 0000000000..80e5148c97
--- /dev/null
+++ b/frameworks/angular-slickgrid/test/cypress/e2e/example51.cy.ts
@@ -0,0 +1,382 @@
+import { format } from '@formkit/tempo';
+
+describe('Example 51 - Menus with Slots', () => {
+ const fullTitles = ['Title', 'Duration', 'Start', 'Finish', 'Cost', '% Complete', 'Action'];
+
+ it('should display Example title', () => {
+ cy.visit(`${Cypress.config('baseUrl')}/example51`);
+ cy.get('h2').should('contain', 'Example 51: Menus with Slots');
+ });
+
+ it('should have exact column titles in the grid', () => {
+ cy.get('#grid51')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(fullTitles[index]));
+ });
+
+ it('should open Context Menu hover "Duration" column and expect built-in and custom items listed in specific order', () => {
+ cy.get('[data-row="0"] > .slick-cell:nth(2)').rightclick({ force: true });
+
+ // 1st item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('.edit-cell-icon').contains('✎');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item span').contains('Edit Cell');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('kbd.edit-cell').contains('F2');
+
+ // icon should rotate while hovering
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0)').trigger('mouseover');
+ cy.wait(175); // wait for rotation
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item .edit-cell-icon')
+ .invoke('css', 'transform') // Get the transform property
+ .then((cssTransform) => {
+ const transformValue = cssTransform as unknown as string; // Cast to string
+ cy.getTransformValue(transformValue, true, 'rotate').then((rotationAngle) => {
+ expect(rotationAngle).to.approximately(13, 15); // 15 degrees rotation
+ });
+ });
+
+ // 2nd item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item').find('i.mdi-content-copy').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item').find('span.menu-item-label').contains('Copy');
+
+ // 3rd item - divider
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(2)').should('have.class', 'slick-menu-item-divider');
+
+ // 4th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(3)').should('have.class', 'slick-menu-item-disabled');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-close').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Clear all Grouping');
+
+ // 5th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-disabled');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item').find('i.mdi-arrow-collapse').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Collapse all Groups');
+
+ // 6th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(5)').should('have.class', 'slick-menu-item-disabled');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item').find('i.mdi-arrow-expand').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Expand all Groups');
+
+ // 7th item - divider
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(6)').should('have.class', 'slick-menu-item-divider');
+
+ // 8th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item').find('i.mdi-download').should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Export');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(7)').find('.sub-item-chevron').should('exist');
+
+ // 9th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(8)').should('have.class', 'slick-menu-item-divider');
+
+ // 10th item
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(9) .menu-item')
+ .find('i.mdi-delete.text-danger')
+ .should('exist');
+ cy.get('.slick-context-menu .slick-menu-command-list .slick-menu-item:nth(9) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Delete Row');
+ });
+
+ it('should open Export->Excel context sub-menu', () => {
+ const subCommands1 = ['Export as Excel', 'Export as CSV', 'Export as PDF'];
+
+ const stub = cy.stub();
+ cy.on('window:alert', stub);
+
+ cy.get('[data-row="0"] > .slick-cell:nth(2)').should('contain', '0');
+ cy.get('[data-row="0"] > .slick-cell:nth(2)').rightclick({ force: true });
+
+ cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains(/^Export$/)
+ .trigger('mouseover');
+
+ cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list')
+ .should('exist')
+ .find('.slick-menu-item .menu-item')
+ .each(($command, index) => expect($command.text()).to.contain(subCommands1[index]));
+
+ // click different sub-menu
+ cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains('Export as Excel')
+ .should('exist')
+ .click()
+ .then(() => expect(stub.getCall(0)).to.be.calledWith('Export to Excel'));
+
+ cy.get('.slick-submenu').should('have.length', 0);
+ });
+
+ it('should open Header Menu from the "Title" column and expect some commands to have keyboard hints on the right side', () => {
+ cy.get('.slick-header-column:nth(0)').trigger('mouseover', { force: true });
+ cy.get('.slick-header-column:nth(0)').children('.slick-header-menu-button').invoke('show').click();
+
+ // 1st item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('i.mdi-arrow-expand-horizontal')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Resize by Content');
+
+ // 2nd item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(1)').should('have.class', 'slick-menu-item-divider');
+
+ // 3rd item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('i.mdi-sort-ascending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Ascending');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('kbd.key-hint').contains('Alt+↑');
+
+ // 4th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-sort-descending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Descending');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('kbd.key-hint').contains('Alt+↓');
+
+ // 5th item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-divider');
+
+ // 6th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('i.mdi-filter-remove-outline')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Filter');
+
+ // 7th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('i.mdi-sort-variant-off').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Sort');
+
+ // 8th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item').find('i.mdi-close').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Hide Column');
+ });
+
+ it('should open Header Menu from the "Duration" column and expect some commands to have tags on the right side', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover', { force: true });
+ cy.get('.slick-header-column:nth(1)').children('.slick-header-menu-button').invoke('show').click();
+
+ // 1st item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('i.mdi-arrow-expand-horizontal')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Resize by Content');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('span.key-hint.danger').contains('NEW');
+
+ // 2nd item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(1)').should('have.class', 'slick-menu-item-divider');
+
+ // 3rd item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('i.mdi-sort-ascending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Ascending');
+
+ // 4th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-sort-descending').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Sort Descending');
+
+ // 5th item - divider
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-divider');
+
+ // 6th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('i.mdi-filter-remove-outline')
+ .should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Filter');
+
+ // 7th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('i.mdi-sort-variant-off').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Remove Sort');
+
+ // 8th item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item').find('i.mdi-close').should('exist');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(7) .menu-item')
+ .find('span.menu-item-label')
+ .contains('Hide Column');
+ });
+
+ it('should open Header Menu from the "Cost" column and expect first item to have a dynamic tooltip timestamp when hovering', () => {
+ cy.get('#grid51').find('.slick-header-column:nth(4)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click();
+
+ // 1st item
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('.advanced-export-icon').contains('📊');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').contains('Advanced Export');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('kbd.key-hint').contains('Ctrl+E');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').should(
+ 'have.css',
+ 'background-color',
+ 'rgba(0, 0, 0, 0)'
+ );
+
+ // icon should scale up
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').trigger('mouseover');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item .advanced-export-icon')
+ .invoke('css', 'transform')
+ .then((cssTransform) => {
+ const transformValue = cssTransform as unknown as string; // Cast to string
+ cy.getTransformValue(transformValue, true, 'scale').then((scaleValue) => {
+ expect(scaleValue).to.be.approximately(1.1, 1.15); // Check the scale value if applied
+ });
+ });
+
+ const today = format(new Date(), 'YYYY-MM-DD');
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').should(
+ 'have.css',
+ 'background-color',
+ 'rgb(133, 70, 133)'
+ );
+ cy.get('.slick-custom-tooltip').contains(`📈 Export timestamp: ${today}`);
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(0)').trigger('mouseout');
+ cy.get('.slick-custom-tooltip').should('not.exist');
+ cy.get('body').click();
+ });
+
+ it('should open Action Menu from last column "Action" column and expect custom items listed in specific order', () => {
+ cy.get('[data-row="1"] > .slick-cell:nth(6)').click();
+ cy.get('.slick-command-header.with-title.with-close').contains('Cell Actions');
+
+ // 1st item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item').find('.mdi-content-copy').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item span').contains('Copy Cell Value');
+
+ // 2nd item - divider
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(1)').should('have.class', 'slick-menu-item-divider');
+
+ // 3rd item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').find('.mdi-download').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item span').contains('Export Row');
+
+ // 4th item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('i.mdi-download').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').find('span.menu-item-label').contains('Export');
+
+ // 5th item - divider
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(4)').should('have.class', 'slick-menu-item-divider');
+
+ // 6th item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item').find('.edit-cell-icon').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item .edit-cell-icon').contains('✎');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(5)').should('have.css', 'background-color', 'rgba(0, 0, 0, 0)');
+
+ // 7th item
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('.mdi-delete.text-danger').should('exist');
+ cy.get('.slick-cell-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item span').contains('Delete Row');
+ });
+
+ it('should open Export->Excel cell sub-menu', () => {
+ const subCommands1 = ['Export as Excel', 'Export as CSV', 'Export as PDF'];
+
+ const stub = cy.stub();
+ cy.on('window:alert', stub);
+
+ cy.get('.slick-cell-menu.slick-menu-level-0 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains(/^Export$/)
+ .trigger('mouseover');
+
+ cy.get('.slick-cell-menu.slick-menu-level-1 .slick-menu-command-list')
+ .should('exist')
+ .find('.slick-menu-item .menu-item')
+ .each(($command, index) => expect($command.text()).to.contain(subCommands1[index]));
+
+ // click different sub-menu
+ cy.get('.slick-cell-menu.slick-menu-level-1 .slick-menu-command-list')
+ .find('.slick-menu-item .menu-item')
+ .contains('Export as Excel')
+ .should('exist')
+ .click()
+ .then(() => expect(stub.getCall(0)).to.be.calledWith('Export row #1 to Excel'));
+
+ cy.get('.slick-submenu').should('have.length', 0);
+ });
+
+ it('should open Grid Menu and expect built-in commands first then custom items listed in specific order', () => {
+ cy.get('.slick-grid-menu-button.mdi-menu').click();
+
+ // 1st item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item')
+ .find('.mdi-filter-remove-outline.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(0) .menu-item span').contains('Clear all Filters');
+
+ // 2nd item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item')
+ .find('.mdi-sort-variant-off.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(1) .menu-item span').contains('Clear all Sorting');
+
+ // 3rd item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item')
+ .find('.mdi-flip-vertical.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item span').contains('Toggle Filter Row');
+
+ // 4th item - divider
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(3)').should('have.class', 'slick-menu-item-divider');
+
+ // 5th item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item')
+ .find('.mdi-file-excel-outline.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(4) .menu-item span').contains('Export to Excel');
+
+ // 6th item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item')
+ .find('.mdi-download.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item span').contains('Export to CSV');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(5) .menu-item').find('span.key-hint.warn').contains('CUSTOM');
+
+ // 7th item
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item')
+ .find('.mdi-refresh.menu-item-icon')
+ .should('exist');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item span').contains('Refresh Data');
+ cy.get('.slick-grid-menu .slick-menu-command-list .slick-menu-item:nth(6) .menu-item').find('kbd.key-hint').contains('F5');
+ });
+
+ it('should sort ascending "Duration" even though the header menu item was override without an action callback', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click();
+
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(2) .menu-item').should('contain', 'Sort Ascending').click();
+
+ cy.get('[data-row=0]').children('.slick-cell:nth(1)').should('contain', '0');
+ cy.get('[data-row=1]').children('.slick-cell:nth(1)').should('contain', '0');
+ cy.get('[data-row=2]').children('.slick-cell:nth(1)').should('contain', '0');
+ });
+
+ it('should sort descending "Duration" even though the header menu item was override without an action callback', () => {
+ cy.get('.slick-header-column:nth(1)').trigger('mouseover').children('.slick-header-menu-button').invoke('show').click();
+
+ cy.get('.slick-header-menu .slick-menu-command-list .slick-menu-item:nth(3) .menu-item').should('contain', 'Sort Descending').click();
+
+ cy.get('[data-row=0]').children('.slick-cell:nth(1)').should('contain', '100');
+ cy.get('[data-row=1]').children('.slick-cell:nth(1)').should('contain', '100');
+ cy.get('[data-row=2]').children('.slick-cell:nth(1)').should('contain', '100');
+ });
+});
diff --git a/frameworks/angular-slickgrid/test/cypress/support/commands.ts b/frameworks/angular-slickgrid/test/cypress/support/commands.ts
index 353157c927..5b395aa95c 100644
--- a/frameworks/angular-slickgrid/test/cypress/support/commands.ts
+++ b/frameworks/angular-slickgrid/test/cypress/support/commands.ts
@@ -46,6 +46,7 @@ declare global {
): Chainable>;
saveLocalStorage: () => void;
restoreLocalStorage: () => void;
+ getTransformValue(cssTransformMatrix: string, absoluteValue: boolean, transformType?: 'rotate' | 'scale'): Chainable;
}
}
}
@@ -86,3 +87,35 @@ Cypress.Commands.add('getNthCell', (row, nthCol, viewport = 'topLeft', { parentS
`${parentSelector} ${canvasSelectorX}${canvasSelectorY} [style="transform: translateY(${row * rowHeight}px);"] > .slick-cell:nth(${nthCol})`
);
});
+
+Cypress.Commands.add(
+ 'getTransformValue',
+ (
+ cssTransformMatrix: string,
+ absoluteValue: boolean,
+ transformType: 'rotate' | 'scale' = 'rotate' // Default to 'rotate'
+ ): Cypress.Chainable => {
+ if (!cssTransformMatrix || cssTransformMatrix === 'none') {
+ throw new Error('Transform matrix is undefined or none');
+ }
+
+ const cssTransformMatrixIndexes = cssTransformMatrix.split('(')[1].split(')')[0].split(',');
+
+ if (transformType === 'rotate') {
+ const cssTransformScale = Math.sqrt(
+ +cssTransformMatrixIndexes[0] * +cssTransformMatrixIndexes[0] + +cssTransformMatrixIndexes[1] * +cssTransformMatrixIndexes[1]
+ );
+
+ const cssTransformSin = +cssTransformMatrixIndexes[1] / cssTransformScale;
+ const cssTransformAngle = Math.round(Math.asin(cssTransformSin) * (180 / Math.PI));
+
+ return cy.wrap(absoluteValue ? Math.abs(cssTransformAngle) : cssTransformAngle);
+ } else if (transformType === 'scale') {
+ // Assuming scale is based on the first value in the matrix.
+ const scaleValue = +cssTransformMatrixIndexes[0]; // First value typically represents scaling in x direction.
+ return cy.wrap(scaleValue); // Directly return the scale value.
+ }
+
+ throw new Error('Unsupported transform type');
+ }
+);
diff --git a/frameworks/aurelia-slickgrid/docs/column-functionalities/cell-menu.md b/frameworks/aurelia-slickgrid/docs/column-functionalities/cell-menu.md
index fe68977b51..5d6810e225 100644
--- a/frameworks/aurelia-slickgrid/docs/column-functionalities/cell-menu.md
+++ b/frameworks/aurelia-slickgrid/docs/column-functionalities/cell-menu.md
@@ -140,6 +140,11 @@ this.gridOptions = {
};
```
+### Custom Menu Item Rendering
+For advanced customization of menu item appearance, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks to create custom HTML or HTMLElement content for your menu items. This allows you to add badges, keyboard shortcuts, status indicators, and more.
+
+See [Custom Menu Slots](../../menu-slots.md) for detailed examples and best practices on rendering custom menu item content across all menu types.
+
### Override Callback Methods
What if you want to dynamically disable or hide a Command/Option or even disable the entire menu in certain circumstances? For these cases, you would use the override callback methods, the method must return a `boolean`. The list of override available are the following
- `menuUsabilityOverride` returning false would make the Cell Menu unavailable to the user
@@ -152,10 +157,7 @@ For example, say we want the Cell Menu to only be available on the first 20 rows
this.columnDefinitions = [
{ id: 'action', field: 'action', name: 'Action',
cellMenu: {
- menuUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
- return (dataContext.id < 21); // say we want to display the menu only from Task 0 to 20
- },
+ menuUsabilityOverride: (args) => args?.dataContext.id < 21, // say we want to display the menu only from Task 0 to 20
}
}
];
@@ -167,12 +169,13 @@ this.columnDefinitions = [
{ id: 'action', field: 'action', name: 'Action',
cellMenu: {
optionItems: [
- {
- option: 0, title: 'n/a', textCssClass: 'italic',
- // only enable this option when the task is Not Completed
- itemUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
- return !dataContext.completed;
+ {
+ option: 0, title: 'n/a', textCssClass: 'italic',
+ // only enable this option when the task is Not Completed
+ itemUsabilityOverride: (args) => {
+ const dataContext = args?.dataContext;
+ return !dataContext.completed;
+ },
},
{ option: 1, iconCssClass: 'mdi mdi-star-outline yellow', title: 'Low' },
{ option: 2, iconCssClass: 'mdi mdi-star orange', title: 'Medium' },
diff --git a/frameworks/aurelia-slickgrid/docs/column-functionalities/editors.md b/frameworks/aurelia-slickgrid/docs/column-functionalities/editors.md
index 4d3b058060..28217c5797 100644
--- a/frameworks/aurelia-slickgrid/docs/column-functionalities/editors.md
+++ b/frameworks/aurelia-slickgrid/docs/column-functionalities/editors.md
@@ -231,7 +231,7 @@ this.columnDefinitions = [
```
### Editor Options (`MultipleSelectOption` interface)
-All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select.js` library.
+All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select-vanilla` library.
```ts
editor: {
@@ -351,8 +351,8 @@ this.columnDefinitions = [
];
```
-### `multiple-select.js` Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+### `multiple-select-vanilla` Options
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit Aurelia-SlickGrid needs, which is why it points to `aurelia-slickgrid/lib` folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -378,7 +378,7 @@ this.columnDefinitions = [
collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }],
model: Editors.singleSelect,
elementOptions: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
}
@@ -449,8 +449,8 @@ So if we take all of these informations and we want to create our own Custom Edi
```ts
const myCustomTitleValidator: EditorValidator = (value: any, args: EditorArgs) => {
// you can get the Editor Args which can be helpful, e.g. we can get the Translate Service from it
- const grid = args && args.grid;
- const gridOptions = (grid && grid.getOptions) ? grid.getOptions() : {};
+ const grid = args.grid;
+ const gridOptions = grid.getOptions() : {};
const i18n = gridOptions.i18n;
if (value == null || value === undefined || !value.length) {
diff --git a/frameworks/aurelia-slickgrid/docs/column-functionalities/editors/select-dropdown-editor.md b/frameworks/aurelia-slickgrid/docs/column-functionalities/editors/select-dropdown-editor.md
index 493ea40de3..c89db6de90 100644
--- a/frameworks/aurelia-slickgrid/docs/column-functionalities/editors/select-dropdown-editor.md
+++ b/frameworks/aurelia-slickgrid/docs/column-functionalities/editors/select-dropdown-editor.md
@@ -6,11 +6,11 @@
- [Collection Label Prefix/Suffix](#collection-label-prefixsuffix)
- [Collection Label Render HTML](#collection-label-render-html)
- [Collection Change Watch](#collection-watch)
- - [`multiple-select.js` Options](#multiple-selectjs-options)
+ - [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- See the [Editors - Wiki](../Editors.md) for more general info about Editors (validators, event handlers, ...)
## Select Editors
-The library ships with two select editors: `singleSelectEditor` and the `multipleSelectEditor`. Both support the [multiple-select](https://github.com/ghiscoding/multiple-select-adapted/blob/master/src/multiple-select.js) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/collectionFormatter.ts).
+The library ships with two select editors: `singleSelectEditor` and the `multipleSelectEditor`. Both support the [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/collectionFormatter.ts).
We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla).
@@ -37,7 +37,7 @@ this.columnDefinitions = [
```
### Editor Options (`MultipleSelectOption` interface)
-All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select.js` library.
+All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select-vanilla` library.
```ts
editor: {
@@ -191,7 +191,7 @@ this.columnDefinitions = [
```
### `multiple-select-vanilla.js` Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/lib` folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -217,7 +217,7 @@ this.columnDefinitions = [
collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }],
model: Editors.singleSelect,
elementOptions: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
}
diff --git a/frameworks/aurelia-slickgrid/docs/column-functionalities/filters/custom-filter.md b/frameworks/aurelia-slickgrid/docs/column-functionalities/filters/custom-filter.md
index b6a752eff8..0776a2acc5 100644
--- a/frameworks/aurelia-slickgrid/docs/column-functionalities/filters/custom-filter.md
+++ b/frameworks/aurelia-slickgrid/docs/column-functionalities/filters/custom-filter.md
@@ -13,7 +13,7 @@ You can also create your own Custom Filter with any html/css you want and/or jQu
- as mentioned in the description, only html/css and/or jQuery libraries are supported.
- this mainly mean that Aurelia templates (Views) are not supported (feel free to contribute).
- SlickGrid uses `table-cell` as CSS for it to display a consistent height for each rows (this keeps the same row height/line-height to always be the same).
- - all this to say that you might be in a situation were your filter shows in the back of the grid. The best approach to overcome this is to use a modal if you can or if the library support `append to body container`. For example, you can see that `multiple-select.js` support a `container` and is needed for the filter to work as can be seen in the `multipleSelectFilter.ts`
+ - all this to say that you might be in a situation were your filter shows in the back of the grid. The best approach to overcome this is to use a modal if you can or if the library support `append to body container`. For example, you can see that `multiple-select-vanilla` support a `container` and is needed for the filter to work as can be seen in the `multipleSelectFilter.ts`
### How to use Custom Filter?
1. You first need to create a `class` using the [Filter interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/filter.interface.ts). Make sure to create all necessary public properties and functions.
diff --git a/frameworks/aurelia-slickgrid/docs/column-functionalities/filters/select-filter.md b/frameworks/aurelia-slickgrid/docs/column-functionalities/filters/select-filter.md
index 9e42c14bed..e879ef9c0f 100644
--- a/frameworks/aurelia-slickgrid/docs/column-functionalities/filters/select-filter.md
+++ b/frameworks/aurelia-slickgrid/docs/column-functionalities/filters/select-filter.md
@@ -16,7 +16,7 @@
- [Collection Async Load](#collection-async-load)
- [Collection Lazy Load](#collection-lazy-load)
- [Collection Watch](#collection-watch)
-- [`multiple-select.js` Options](#multiple-selectjs-options)
+- [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- [Filter Options (`MultipleSelectOption` interface)](#filter-options-multipleselectoption-interface)
- [Display shorter selected label text](#display-shorter-selected-label-text)
- [Query against a different field](#query-against-another-field-property)
@@ -36,7 +36,7 @@ Multiple Select (dropdown) filter is useful when we want to filter the grid 1 or
We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla).
#### Note
-For this filter to work you will need to add [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) to your project. This is a customized version of the original (thought all the original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options). Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/dist/lib` folder. This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original)
+For this filter to work you will need to add [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) to your project. This is a customized version of the original (thought all the original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options). Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/dist/lib` folder. This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original)
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
- `okButtonText` was also added for locale (i18n)
- `offsetLeft` option was added to make it possible to offset the dropdown. By default it is set to 0 and is aligned to the left of the select element. This option is particularly helpful when used as the last right column, not to fall off the screen.
@@ -579,7 +579,7 @@ this.columnDefinitions = [
```
### Filter Options (`MultipleSelectOption` interface)
-All the available options that can be provided as filter `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your filter `options` to that interface to make sure that you use only valid options of the `multiple-select.js` library.
+All the available options that can be provided as filter `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your filter `options` to that interface to make sure that you use only valid options of the `multiple-select-vanilla` library.
```ts
filter: {
@@ -602,8 +602,8 @@ this.gridOptions = {
}
```
-### Multiple-select.js Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your filter `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+### multiple-select-vanilla Options
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your filter `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit SlickGrid-Universal needs, which is why we are using a fork [ghiscoding/multiple-select-modified](https://github.com/ghiscoding/multiple-select-modified) folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -632,7 +632,7 @@ this.columnDefinitions = [
model: Filters.singleSelect,
// previously known as `filterOptions` for < 9.0
options: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
} as MultipleSelectOption
diff --git a/frameworks/aurelia-slickgrid/docs/grid-functionalities/composite-editor-modal.md b/frameworks/aurelia-slickgrid/docs/grid-functionalities/composite-editor-modal.md
index fbd5d12c06..c7f58c74d6 100644
--- a/frameworks/aurelia-slickgrid/docs/grid-functionalities/composite-editor-modal.md
+++ b/frameworks/aurelia-slickgrid/docs/grid-functionalities/composite-editor-modal.md
@@ -476,7 +476,7 @@ export class GridExample {
// you can also change some editor options
// not all Editors supports this functionality, so far only these Editors are supported: AutoComplete, Date, Single/Multiple Select
if (columnDef.id === 'completed') {
- this.compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select.js, show filter in dropdown
+ this.compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select-vanilla, show filter in dropdown
this.compositeEditorInstance.changeFormEditorOption('product', 'minLength', 3); // autocomplete, change minLength char to type
this.compositeEditorInstance.changeFormEditorOption('finish', 'displayDateMin', 'today'); // calendar picker, change minDate to today
}
diff --git a/frameworks/aurelia-slickgrid/docs/grid-functionalities/context-menu.md b/frameworks/aurelia-slickgrid/docs/grid-functionalities/context-menu.md
index 2e77bed499..7906c2b194 100644
--- a/frameworks/aurelia-slickgrid/docs/grid-functionalities/context-menu.md
+++ b/frameworks/aurelia-slickgrid/docs/grid-functionalities/context-menu.md
@@ -23,7 +23,7 @@ This extensions is wrapped around the new SlickGrid Plugin **SlickContextMenu**
### Default Usage
Technically, the Context Menu is enabled by default (copy, export) and so you don't have anything to do to enjoy it (you could disable it at any time). However, if you want to customize the content of the Context Menu, then continue reading. You can customize the menu with 2 different lists, Commands and/or Options, they can be used separately or at the same time. Also note that even though the code shown below makes a separation between the Commands and Options, you can mix them in the same Context Menu.
-#### with Commands
+#### with Commands (Static)
```ts
this.gridOptions = {
@@ -63,6 +63,98 @@ this.gridOptions = {
};
```
+#### with Commands (Dynamic Builder)
+For advanced scenarios where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list.
+
+```ts
+this.gridOptions = {
+ enableContextMenu: true,
+ contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ // Example: Add custom commands after built-in ones
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'delete-row',
+ title: 'Delete Row',
+ iconCssClass: 'mdi mdi-delete text-danger',
+ action: (e, args) => {
+ if (confirm(`Delete row ${args.dataContext.id}?`)) {
+ this.gridService.deleteItem(args.dataContext);
+ }
+ }
+ },
+ {
+ command: 'duplicate-row',
+ title: 'Duplicate Row',
+ iconCssClass: 'mdi mdi-content-duplicate',
+ action: (e, args) => {
+ const newItem = { ...args.dataContext, id: this.generateNewId() };
+ this.gridService.addItem(newItem);
+ }
+ }
+ ];
+ },
+ onCommand: (e, args) => {
+ // Handle commands here if not using action callbacks
+ console.log('Command:', args.command);
+ }
+ }
+};
+```
+
+**Example: Filter commands based on row data**
+```ts
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ // You can't access row data here, but you can filter/modify built-in items
+ // Use itemUsabilityOverride or itemVisibilityOverride for row-specific logic
+
+ // Only show export commands
+ return builtInItems.filter(item =>
+ item === 'divider' || item.command?.includes('export')
+ );
+ }
+}
+```
+
+**Example: Sort and reorganize commands**
+```ts
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ const customFirst = [
+ {
+ command: 'edit',
+ title: 'Edit Row',
+ iconCssClass: 'mdi mdi-pencil',
+ positionOrder: 0
+ }
+ ];
+
+ // Sort built-in commands by positionOrder
+ const sorted = [...builtInItems].sort((a, b) => {
+ if (a === 'divider' || b === 'divider') return 0;
+ return (a.positionOrder || 50) - (b.positionOrder || 50);
+ });
+
+ return [...customFirst, 'divider', ...sorted];
+ }
+}
+```
+
+**When to use `commandListBuilder` vs `commandItems`:**
+- Use `commandItems` for static command lists
+- Use `commandListBuilder` when you need to:
+ - Append/prepend to built-in commands
+ - Filter or modify commands dynamically
+ - Sort or reorder the final command list
+ - Have full control over what gets rendered
+
+**Note:** Typically use `commandListBuilder` **instead of** `commandItems`, not both together.
+
+See the main [Custom Menu Slots](../menu-slots.md) documentation for detailed `commandListBuilder` examples.
+
#### with Options
That is when you want to define a list of Options (only 1 list) that the user can choose from and once is selected we would do something (for example change the value of a cell in the grid).
@@ -79,7 +171,7 @@ this.gridOptions = {
// subscribe to Context Menu onOptionSelected event (or use the "action" callback on each option)
onOptionSelected: (e, args) => {
// change Priority
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext && dataContext.hasOwnProperty('priority')) {
dataContext.priority = args.item.option;
this.sgb.gridService.updateItem(dataContext);
@@ -139,7 +231,7 @@ For example, say we want the Context Menu to only be available on the first 20 r
```ts
contextMenu: {
menuUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return (dataContext.id < 21); // say we want to display the menu only from Task 0 to 20
},
},
@@ -153,7 +245,7 @@ contextMenu: {
option: 0, title: 'n/a', textCssClass: 'italic',
// only enable this option when the task is Not Completed
itemUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
},
@@ -177,6 +269,11 @@ contextMenu: {
}
```
+### Custom Menu Item Rendering
+For advanced customization of menu item appearance, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks to create custom HTML or HTMLElement content for your menu items. This allows you to add badges, keyboard shortcuts, status indicators, and more.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content across all menu types.
+
### Show Menu only over Certain Columns
Say you want to show the Context Menu only when the user is over certain columns of the grid. For that, you could use the `commandShownOverColumnIds` (or `optionShownOverColumnIds`) array, by default these arrays are empty and when that is the case then the menu will be accessible from any columns. So if we want to have the Context Menu available only over the first 2 columns, we would have an array of those 2 column ids. For example, the following would show the Context Menu everywhere except the last 2 columns (priority, action) since they are not part of the array.
```ts
diff --git a/frameworks/aurelia-slickgrid/docs/grid-functionalities/grid-menu.md b/frameworks/aurelia-slickgrid/docs/grid-functionalities/grid-menu.md
index 45b6ff831f..79ec2166a0 100644
--- a/frameworks/aurelia-slickgrid/docs/grid-functionalities/grid-menu.md
+++ b/frameworks/aurelia-slickgrid/docs/grid-functionalities/grid-menu.md
@@ -73,6 +73,11 @@ this.gridOptions = {
};
```
+#### Advanced: Dynamic Command List Builder
+For more advanced use cases where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list.
+
+See the main [Custom Menu Slots](../menu-slots.md) documentation for detailed `commandListBuilder` examples.
+
#### Events
There are multiple events/callback hooks which are accessible from the Grid Options
- `onBeforeMenuShow`
@@ -105,6 +110,11 @@ gridMenu: {
For more info on all the available properties of the custom commands, you can read refer to the doc written in the Grid Menu [implementation](https://github.com/6pac/SlickGrid/blob/master/controls/slick.gridmenu.js) itself.
+### Custom Menu Item Rendering
+To customize the appearance of menu items with custom HTML, badges, icons, or interactive elements, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content.
+
### How to change an icon of all default commands?
You can change any of the default command icon(s) by changing the `icon[X-command]`, for example, see below for the defaults.
```ts
diff --git a/frameworks/aurelia-slickgrid/docs/grid-functionalities/header-menu-header-buttons.md b/frameworks/aurelia-slickgrid/docs/grid-functionalities/header-menu-header-buttons.md
index 04e75c5cf5..2a12bea3f8 100644
--- a/frameworks/aurelia-slickgrid/docs/grid-functionalities/header-menu-header-buttons.md
+++ b/frameworks/aurelia-slickgrid/docs/grid-functionalities/header-menu-header-buttons.md
@@ -20,7 +20,10 @@ The Header Menu also comes, by default, with a list of built-in custom commands
- Sort Descending (you can hide it with `hideSortCommands: true`)
- Hide Column (you can hide it with `hideColumnHideCommand: true`)
-This section is called Custom Commands because you can also customize this section with your own commands. To do that, you need to fill in 2 properties (an array of `headerMenuItems` that will go under each column definition and define `onCommand` callbacks) in your Grid Options. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
+This section is called Custom Commands because you can also customize this section with your own commands. You can do this in two ways: using static command items or using a dynamic command list builder.
+
+#### Static Command Items
+To add static commands, fill in an array of items in your column definition's `header.menu.commandItems`. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
```ts
this.gridOptions = {
enableAutoResize: true,
@@ -51,6 +54,100 @@ this.gridOptions = {
}
};
```
+
+#### Dynamic Command List Builder
+For advanced use cases where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list. This is executed **after** `commandItems` and is the **last call before rendering**.
+
+```ts
+this.columnDefinitions = [
+ {
+ id: 'title',
+ name: 'Title',
+ field: 'title',
+ header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ // Append custom commands to the built-in sort/hide commands
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'freeze-column',
+ title: 'Freeze Column',
+ iconCssClass: 'mdi mdi-pin',
+ action: (e, args) => {
+ // Implement column freezing
+ console.log('Freeze column:', args.column.name);
+ }
+ }
+ ];
+ }
+ }
+ }
+ }
+];
+```
+
+**Example: Conditional commands based on column type**
+```ts
+header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ const column = this; // column context
+
+ // Add filtering option only for filterable columns
+ const extraCommands = [];
+ if (column.filterable !== false) {
+ extraCommands.push({
+ command: 'clear-filter',
+ title: 'Clear Filter',
+ iconCssClass: 'mdi mdi-filter-remove',
+ action: (e, args) => {
+ this.filterService.clearFilterByColumnId(args.column.id);
+ }
+ });
+ }
+
+ return [...builtInItems, ...extraCommands];
+ }
+ }
+}
+```
+
+**Example: Remove sort commands, keep only custom ones**
+```ts
+header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ // Filter out sort commands
+ const filtered = builtInItems.filter(item =>
+ item !== 'divider' &&
+ !item.command?.includes('sort')
+ );
+
+ // Add custom commands
+ return [
+ ...filtered,
+ {
+ command: 'custom-action',
+ title: 'Custom Action',
+ iconCssClass: 'mdi mdi-star',
+ action: (e, args) => alert('Custom: ' + args.column.name)
+ }
+ ];
+ }
+ }
+}
+```
+
+**When to use `commandListBuilder`:**
+- You want to append/prepend items to built-in commands
+- You need to filter or modify commands based on column properties
+- You want to customize the command list per column dynamically
+- You need full control over the final command list
+
+**Note:** Use `commandListBuilder` **instead of** `commandItems`, not both together.
+
#### Callback Hooks
There are 2 callback hooks which are accessible in the Grid Options
- `onBeforeMenuShow`
@@ -58,6 +155,11 @@ There are 2 callback hooks which are accessible in the Grid Options
For more info on all the available properties of the custom commands, you can read refer to the doc written in the Grid Menu [implementation](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/extensions/slickHeaderButtons.ts) itself.
+### Custom Menu Item Rendering
+To customize the appearance of menu items with custom HTML, badges, icons, or interactive elements, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content.
+
### How to change icon(s) of the default commands?
You can change any of the default command icon(s) by changing the `icon[X-command]`, for example, see below for the defaults.
```ts
diff --git a/frameworks/aurelia-slickgrid/docs/menu-slots.md b/frameworks/aurelia-slickgrid/docs/menu-slots.md
new file mode 100644
index 0000000000..14b9f9bb47
--- /dev/null
+++ b/frameworks/aurelia-slickgrid/docs/menu-slots.md
@@ -0,0 +1,524 @@
+## Custom Menu Slots - Rendering
+
+All menu plugins (Header Menu, Cell Menu, Context Menu, Grid Menu) support **cross-framework compatible slot rendering** for custom content injection in menu items. This is achieved through the `slotRenderer` callback at the item level combined with an optional `defaultMenuItemRenderer` at the menu level.
+
+> **Note:** This documentation covers **how menu items are rendered** (visual presentation). If you need to **dynamically modify which commands appear** in the menu (filtering, sorting, adding/removing items), see the `commandListBuilder` callback documented in [Grid Menu](grid-functionalities/grid-menu.md), [Context Menu](grid-functionalities/context-menu.md), or [Header Menu](grid-functionalities/header-menu-header-buttons.md).
+
+### TypeScript Tip: Type Inference with commandListBuilder
+
+When using `commandListBuilder` to add custom menu items with slotRenderer callbacks, **cast the return value to the appropriate type** to enable proper type parameters in callbacks:
+
+```typescript
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ return [
+ ...builtInItems,
+ {
+ command: 'custom-action',
+ title: 'My Action',
+ slotRenderer: (cmdItem, args) => `
${cmdItem.title}
`,
+ }
+ ] as Array;
+ }
+}
+```
+
+Alternatively, if you only have built-in items and dividers, you can use the simpler cast:
+
+```typescript
+return [...] as Array;
+```
+
+### Core Concept
+
+Each menu item can define a `slotRenderer` callback function that receives the item and args, and returns either an HTML string or an HTMLElement. This single API works uniformly across all menu plugins.
+
+### Slot Renderer Callback
+
+```typescript
+slotRenderer?: (cmdItem: MenuItem, args: MenuCallbackArgs, event?: Event) => string | HTMLElement
+```
+
+- **cmdItem** - The menu cmdItem object containing command, title, iconCssClass, etc.
+- **args** - The callback args providing access to grid, column, dataContext, and other context
+- **event** - Optional DOM event passed during click handling (allows `stopPropagation()`)
+
+### Basic Example - HTML String Rendering
+
+```typescript
+const menuItem = {
+ command: 'custom-command',
+ title: 'Custom Action',
+ iconCssClass: 'mdi mdi-star',
+ // Return custom HTML string for the entire menu item
+ slotRenderer: () => `
+
+
+ Custom Action
+ NEW
+
+ `
+};
+```
+
+### Advanced Example - HTMLElement Objects
+
+```typescript
+// Create custom element with full DOM control
+const menuItem = {
+ command: 'notifications',
+ title: 'Notifications',
+ // Return HTMLElement for more control and event listeners
+ slotRenderer: (cmdItem, args) => {
+ const container = document.createElement('div');
+ container.style.display = 'flex';
+ container.style.alignItems = 'center';
+
+ const icon = document.createElement('i');
+ icon.className = 'mdi mdi-bell';
+ icon.style.marginRight = '8px';
+
+ const text = document.createElement('span');
+ text.textContent = cmdItem.title;
+
+ const badge = document.createElement('span');
+ badge.className = 'badge';
+ badge.textContent = '5';
+ badge.style.marginLeft = 'auto';
+
+ container.appendChild(icon);
+ container.appendChild(text);
+ container.appendChild(badge);
+
+ return container;
+ }
+};
+```
+
+### Default Menu-Level Renderer
+
+Set a `defaultMenuItemRenderer` at the menu option level to apply to all items (unless overridden by individual `slotRenderer`):
+
+```typescript
+const menuOption = {
+ // Apply this renderer to all menu items (can be overridden per item)
+ defaultMenuItemRenderer: (cmdItem, args) => {
+ return `
+
';
+ }
+ }
+}
+```
diff --git a/frameworks/aurelia-slickgrid/docs/migrations/migration-to-10.x.md b/frameworks/aurelia-slickgrid/docs/migrations/migration-to-10.x.md
index e33e944d9a..99c5b21c51 100644
--- a/frameworks/aurelia-slickgrid/docs/migrations/migration-to-10.x.md
+++ b/frameworks/aurelia-slickgrid/docs/migrations/migration-to-10.x.md
@@ -1,4 +1,4 @@
-## Cleaner Code / Smaller Code ⚡
+## Simplification and Modernization ⚡
One of the biggest change of this release is to hide columns by using the `hidden` column property (now used by Column Picker, Grid Menu, etc...). Previously we were removing columns from the original columns array and we then called `setColumns()` to update the grid, but this meant that we had to keep references for all visible and non-visible columns. With this new release we now keep the full columns array at all time and instead we just change column(s) visibility via their `hidden` column properties by using `grid.updateColumnById('id', { hidden: true })` and finally we update the grid via `grid.updateColumns()`. What I'm trying to emphasis is that you should really stop using `grid.setColumns()` in v10+, and if you want to hide some columns when declaring the columns, then just update their `hidden` properties, see more details below...
@@ -85,11 +85,30 @@ gridOptions = {
};
```
-### External Resources are now auto-enabled
-
This change does not require any code update from the end user, but it is a change that you should probably be aware of nonetheless. The reason I decided to implement this is because I often forget myself to enable the associated flag and typically if you wanted to load the resource, then it's most probably because you also want it enabled. So for example, if your register `ExcelExportService` then the library will now auto-enable the resource with its associated flag (which in this case is `enableExcelExport:true`)... unless you already disabled the flag (or enabled) yourself, if so then the internal assignment will simply be skipped and yours will prevail. Also just to be clear, the list of auto-enabled external resources is rather small, it will auto-enable the following resources:
(ExcelExportService, PdfExportService, TextExportService, CompositeEditorComponent and RowDetailView).
+### Menu with Commands
+
+All menu plugins (Cell Menu, Context Menu, Header Menu and Grid Menu) now have a new `commandListBuilder: (items) => items` which now allow you to filter/sort and maybe override built-in commands. With this new feature in place, I'm deprecating all `hide...` properties and also `positionOrder` since you can now do that with the builder. You could also use the `hideCommands` which accepts an array of built-in command names. This well remove huge amount of `hide...` properties (over 30) that keeps increasing anytime a new built-in command gets added (in other words, this will simplify maintenance for both you and me).
+
+These are currently just deprecations in v10.x but it's strongly recommended to start using the `commandListBuilder` and/or `hideCommands` and move away from the deprecated properties which will be removed in v11.x. For example if we want to hide some built-in commands:
+
+```diff
+gridOptions = {
+ gridMenu: {
+- hideExportCsvCommand: true,
+- hideTogglePreHeaderCommand: true,
+
+// via command name(s)
++ hideCommands: ['export-csv', 'toggle-preheader'],
+
+// or via builder
++ commandListBuilder: (cmdItems) => [...cmdItems.filter(x => x !== 'divider' && x.command !== 'export-csv' && x.command !== 'toggle-preheader')]
+ }
+}
+```
+
---
{% hint style="note" %}
diff --git a/frameworks/aurelia-slickgrid/tsconfig.json b/frameworks/aurelia-slickgrid/tsconfig.json
index 82191a3a37..201c1b1dc0 100644
--- a/frameworks/aurelia-slickgrid/tsconfig.json
+++ b/frameworks/aurelia-slickgrid/tsconfig.json
@@ -21,7 +21,6 @@
"skipLibCheck": true,
"sourceMap": true,
"newLine": "lf",
- "downlevelIteration": true,
"outDir": "dist"
},
"include": ["src"],
diff --git a/frameworks/slickgrid-react/docs/column-functionalities/cell-menu.md b/frameworks/slickgrid-react/docs/column-functionalities/cell-menu.md
index ff0b52b027..b9e63d392a 100644
--- a/frameworks/slickgrid-react/docs/column-functionalities/cell-menu.md
+++ b/frameworks/slickgrid-react/docs/column-functionalities/cell-menu.md
@@ -142,6 +142,11 @@ const gridOptions = {
};
```
+### Custom Menu Item Rendering
+For advanced customization of menu item appearance, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks to create custom HTML or HTMLElement content for your menu items. This allows you to add badges, keyboard shortcuts, status indicators, and more.
+
+See [Custom Menu Slots](../../menu-slots.md) for detailed examples and best practices on rendering custom menu item content across all menu types.
+
### Override Callback Methods
What if you want to dynamically disable or hide a Command/Option or even disable the entire menu in certain circumstances? For these cases, you would use the override callback methods, the method must return a `boolean`. The list of override available are the following
- `menuUsabilityOverride` returning false would make the Cell Menu unavailable to the user
@@ -155,10 +160,7 @@ const columnDefinitions = [
{
id: 'action', field: 'action', name: 'Action',
cellMenu: {
- menuUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
- return (dataContext.id < 21); // say we want to display the menu only from Task 0 to 20
- },
+ menuUsabilityOverride: (args) => args?.dataContext.id < 21, // say we want to display the menu only from Task 0 to 20
}
}
];
@@ -171,12 +173,13 @@ const columnDefinitions = [
id: 'action', field: 'action', name: 'Action',
cellMenu: {
optionItems: [
- {
- option: 0, title: 'n/a', textCssClass: 'italic',
- // only enable this option when the task is Not Completed
- itemUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
- return !dataContext.completed;
+ {
+ option: 0, title: 'n/a', textCssClass: 'italic',
+ // only enable this option when the task is Not Completed
+ itemUsabilityOverride: (args) => {
+ const dataContext = args?.dataContext;
+ return !dataContext.completed;
+ },
},
{ option: 1, iconCssClass: 'mdi mdi-star-outline yellow', title: 'Low' },
{ option: 2, iconCssClass: 'mdi mdi-star orange', title: 'Medium' },
diff --git a/frameworks/slickgrid-react/docs/column-functionalities/editors.md b/frameworks/slickgrid-react/docs/column-functionalities/editors.md
index fd6b4043f3..3a3a2a6789 100644
--- a/frameworks/slickgrid-react/docs/column-functionalities/editors.md
+++ b/frameworks/slickgrid-react/docs/column-functionalities/editors.md
@@ -13,7 +13,7 @@
- [Collection Async Load](#collection-async-load)
- [Collection Label Prefix/Suffix](#collection-label-prefixsuffix)
- [Collection Label Render HTML](#collection-label-render-html)
- - [`multiple-select.js` Options](#multiple-selectjs-options)
+ - [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- [Editor Options](#editor-options)
- [Validators](#validators)
- [Custom Validator](#custom-validator)
@@ -226,7 +226,7 @@ const columnDefinitions = [
```
### Editor Options (`MultipleSelectOption` interface)
-All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/lib/src/interfaces/multipleSelectOption.interface.ts) and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select.js` library.
+All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/lib/src/interfaces/multipleSelectOption.interface.ts) and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select-vanilla` library.
```tsx
editor: {
@@ -349,8 +349,8 @@ const columnDefinitions = [
];
```
-### `multiple-select.js` Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+### `multiple-select-vanilla` Options
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit slickgrid-react needs, which is why it points to `slickgrid-react/lib` folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -376,7 +376,7 @@ const columnDefinitions = [
collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }],
model: Editors.singleSelect,
elementOptions: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
}
@@ -447,8 +447,8 @@ So if we take all of these informations and we want to create our own Custom Edi
```tsx
const myCustomTitleValidator: EditorValidator = (value: any, args: EditorArgs) => {
// you can get the Editor Args which can be helpful, e.g. we can get the Translate Service from it
- const grid = args && args.grid;
- const gridOptions = (grid && grid.getOptions) ? grid.getOptions() : {};
+ const grid = args.grid;
+ const gridOptions = grid.getOptions() : {};
const i18n = gridOptions.i18n;
if (value == null || value === undefined || !value.length) {
diff --git a/frameworks/slickgrid-react/docs/column-functionalities/editors/select-dropdown-editor.md b/frameworks/slickgrid-react/docs/column-functionalities/editors/select-dropdown-editor.md
index 9de1f4207e..a81c26710c 100644
--- a/frameworks/slickgrid-react/docs/column-functionalities/editors/select-dropdown-editor.md
+++ b/frameworks/slickgrid-react/docs/column-functionalities/editors/select-dropdown-editor.md
@@ -6,11 +6,11 @@
- [Collection Label Prefix/Suffix](#collection-label-prefixsuffix)
- [Collection Label Render HTML](#collection-label-render-html)
- [Collection Change Watch](#collection-watch)
- - [`multiple-select.js` Options](#multiple-selectjs-options)
+ - [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- See the [Editors - Wiki](../Editors.md) for more general info about Editors (validators, event handlers, ...)
## Select Editors
-The library ships with two select editors: `singleSelectEditor` and the `multipleSelectEditor`. Both support the [multiple-select](https://github.com/ghiscoding/multiple-select-adapted/blob/master/src/multiple-select.js) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/collectionFormatter.ts).
+The library ships with two select editors: `singleSelectEditor` and the `multipleSelectEditor`. Both support the [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/collectionFormatter.ts).
We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla).
@@ -37,7 +37,7 @@ const columnDefinitions = [
```
### Editor Options (`MultipleSelectOption` interface)
-All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select.js` library.
+All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select-vanilla` library.
```ts
editor: {
@@ -191,7 +191,7 @@ const columnDefinitions = [
```
### `multiple-select-vanilla.js` Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/lib` folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -217,7 +217,7 @@ const columnDefinitions = [
collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }],
model: Editors.singleSelect,
elementOptions: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
}
diff --git a/frameworks/slickgrid-react/docs/column-functionalities/filters/custom-filter.md b/frameworks/slickgrid-react/docs/column-functionalities/filters/custom-filter.md
index 561137e42c..efab5a435e 100644
--- a/frameworks/slickgrid-react/docs/column-functionalities/filters/custom-filter.md
+++ b/frameworks/slickgrid-react/docs/column-functionalities/filters/custom-filter.md
@@ -12,7 +12,7 @@ You can also create your own Custom Filter with any html/css you want to use. Re
- as mentioned in the description, only html/css and/or JS libraries are supported.
- this mainly mean that React templates (Views) are not supported (feel free to contribute).
- SlickGrid uses `table-cell` as CSS for it to display a consistent height for each rows (this keeps the same row height/line-height to always be the same).
- - all this to say that you might be in a situation were your filter shows in the back of the grid. The best approach to overcome this is to use a modal if you can or if the library support `append to body container`. For example, you can see that `multiple-select.js` support a `container` and is needed for the filter to work as can be seen in the [multipleSelectFilter.ts](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/filters/multipleSelectFilter.ts#L26)
+ - all this to say that you might be in a situation were your filter shows in the back of the grid. The best approach to overcome this is to use a modal if you can or if the library support `append to body container`. For example, you can see that `multiple-select-vanilla` support a `container` and is needed for the filter to work as can be seen in the [multipleSelectFilter.ts](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/filters/multipleSelectFilter.ts#L26)
### How to use Custom Filter?
1. You first need to create a `class` using the [Filter interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/models/filter.interface.ts). Make sure to create all necessary public properties and functions.
diff --git a/frameworks/slickgrid-react/docs/column-functionalities/filters/select-filter.md b/frameworks/slickgrid-react/docs/column-functionalities/filters/select-filter.md
index 1a0ab4e6f1..37b65bf7cf 100644
--- a/frameworks/slickgrid-react/docs/column-functionalities/filters/select-filter.md
+++ b/frameworks/slickgrid-react/docs/column-functionalities/filters/select-filter.md
@@ -16,7 +16,7 @@
- [Collection Async Load](#collection-async-load)
- [Collection Lazy Load](#collection-lazy-load)
- [Collection Watch](#collection-watch)
-- [`multiple-select.js` Options](#multiple-selectjs-options)
+- [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- [Filter Options (`MultipleSelectOption` interface)](#filter-options-multipleselectoption-interface)
- [Display shorter selected label text](#display-shorter-selected-label-text)
- [Query against a different field](#query-against-another-field-property)
@@ -36,7 +36,7 @@ Multiple Select (dropdown) filter is useful when we want to filter the grid 1 or
We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla).
#### Note
-For this filter to work you will need to add [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) to your project. This is a customized version of the original (thought all the original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options). Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/dist/lib` folder. This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original)
+For this filter to work you will need to add [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) to your project. This is a customized version of the original (thought all the original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options). Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/dist/lib` folder. This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original)
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
- `okButtonText` was also added for locale (i18n)
- `offsetLeft` option was added to make it possible to offset the dropdown. By default it is set to 0 and is aligned to the left of the select element. This option is particularly helpful when used as the last right column, not to fall off the screen.
@@ -601,7 +601,7 @@ const columnDefinitions = [
```
### Filter Options (`MultipleSelectOption` interface)
-All the available options that can be provided as filter `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your filter `options` to that interface to make sure that you use only valid options of the `multiple-select.js` library.
+All the available options that can be provided as filter `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your filter `options` to that interface to make sure that you use only valid options of the `multiple-select-vanilla` library.
```ts
filter: {
@@ -624,8 +624,8 @@ const gridOptions = {
}
```
-### Multiple-select.js Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your filter `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+### multiple-select-vanilla Options
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your filter `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit SlickGrid-Universal needs, which is why we are using a fork [ghiscoding/multiple-select-modified](https://github.com/ghiscoding/multiple-select-modified) folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -653,7 +653,7 @@ const columnDefinitions = [
collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }],
model: Filters.singleSelect,
options: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
} as MultipleSelectOption
diff --git a/frameworks/slickgrid-react/docs/grid-functionalities/composite-editor-modal.md b/frameworks/slickgrid-react/docs/grid-functionalities/composite-editor-modal.md
index 7df59bd2c8..54497a33fc 100644
--- a/frameworks/slickgrid-react/docs/grid-functionalities/composite-editor-modal.md
+++ b/frameworks/slickgrid-react/docs/grid-functionalities/composite-editor-modal.md
@@ -481,7 +481,7 @@ const Example: React.FC = () => {
// you can also change some editor options
// not all Editors supports this functionality, so far only these Editors are supported: AutoComplete, Date, Single/Multiple Select
if (columnDef.id === 'completed') {
- compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select.js, show filter in dropdown
+ compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select-vanilla, show filter in dropdown
compositeEditorInstance.changeFormEditorOption('product', 'minLength', 3); // autocomplete, change minLength char to type
this.compositeEditorInstance.changeFormEditorOption('finish', 'displayDateMin', 'today'); // calendar picker, change minDate to today
}
diff --git a/frameworks/slickgrid-react/docs/grid-functionalities/context-menu.md b/frameworks/slickgrid-react/docs/grid-functionalities/context-menu.md
index c1381ffc06..e07d7bb89d 100644
--- a/frameworks/slickgrid-react/docs/grid-functionalities/context-menu.md
+++ b/frameworks/slickgrid-react/docs/grid-functionalities/context-menu.md
@@ -23,7 +23,7 @@ This extensions is wrapped around the new SlickGrid Plugin **SlickContextMenu**
### Default Usage
Technically, the Context Menu is enabled by default (copy, export) and so you don't have anything to do to enjoy it (you could disable it at any time). However, if you want to customize the content of the Context Menu, then continue reading. You can customize the menu with 2 different lists, Commands and/or Options, they can be used separately or at the same time. Also note that even though the code shown below makes a separation between the Commands and Options, you can mix them in the same Context Menu.
-#### with Commands
+#### with Commands (Static)
```ts
const gridOptions = {
@@ -63,6 +63,98 @@ const gridOptions = {
};
```
+#### with Commands (Dynamic Builder)
+For advanced scenarios where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list.
+
+```ts
+this.gridOptions = {
+ enableContextMenu: true,
+ contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ // Example: Add custom commands after built-in ones
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'delete-row',
+ title: 'Delete Row',
+ iconCssClass: 'mdi mdi-delete text-danger',
+ action: (e, args) => {
+ if (confirm(`Delete row ${args.dataContext.id}?`)) {
+ this.gridService.deleteItem(args.dataContext);
+ }
+ }
+ },
+ {
+ command: 'duplicate-row',
+ title: 'Duplicate Row',
+ iconCssClass: 'mdi mdi-content-duplicate',
+ action: (e, args) => {
+ const newItem = { ...args.dataContext, id: this.generateNewId() };
+ this.gridService.addItem(newItem);
+ }
+ }
+ ];
+ },
+ onCommand: (e, args) => {
+ // Handle commands here if not using action callbacks
+ console.log('Command:', args.command);
+ }
+ }
+};
+```
+
+**Example: Filter commands based on row data**
+```ts
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ // You can't access row data here, but you can filter/modify built-in items
+ // Use itemUsabilityOverride or itemVisibilityOverride for row-specific logic
+
+ // Only show export commands
+ return builtInItems.filter(item =>
+ item === 'divider' || item.command?.includes('export')
+ );
+ }
+}
+```
+
+**Example: Sort and reorganize commands**
+```ts
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ const customFirst = [
+ {
+ command: 'edit',
+ title: 'Edit Row',
+ iconCssClass: 'mdi mdi-pencil',
+ positionOrder: 0
+ }
+ ];
+
+ // Sort built-in commands by positionOrder
+ const sorted = [...builtInItems].sort((a, b) => {
+ if (a === 'divider' || b === 'divider') return 0;
+ return (a.positionOrder || 50) - (b.positionOrder || 50);
+ });
+
+ return [...customFirst, 'divider', ...sorted];
+ }
+}
+```
+
+**When to use `commandListBuilder` vs `commandItems`:**
+- Use `commandItems` for static command lists
+- Use `commandListBuilder` when you need to:
+ - Append/prepend to built-in commands
+ - Filter or modify commands dynamically
+ - Sort or reorder the final command list
+ - Have full control over what gets rendered
+
+**Note:** Typically use `commandListBuilder` **instead of** `commandItems`, not both together.
+
+See the main [Custom Menu Slots](../menu-slots.md) documentation for detailed `commandListBuilder` examples.
+
#### with Options
That is when you want to define a list of Options (only 1 list) that the user can choose from and once is selected we would do something (for example change the value of a cell in the grid).
@@ -79,7 +171,7 @@ const gridOptions = {
// subscribe to Context Menu onOptionSelected event (or use the "action" callback on each option)
onOptionSelected: (e, args) => {
// change Priority
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext && dataContext.hasOwnProperty('priority')) {
dataContext.priority = args.item.option;
reactGridRef.current?.gridService.updateItem(dataContext);
@@ -139,7 +231,7 @@ For example, say we want the Context Menu to only be available on the first 20 r
```ts
contextMenu: {
menuUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return (dataContext.id < 21); // say we want to display the menu only from Task 0 to 20
},
},
@@ -153,7 +245,7 @@ contextMenu: {
option: 0, title: 'n/a', textCssClass: 'italic',
// only enable this option when the task is Not Completed
itemUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
},
@@ -177,6 +269,11 @@ contextMenu: {
}
```
+### Custom Menu Item Rendering
+For advanced customization of menu item appearance, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks to create custom HTML or HTMLElement content for your menu items. This allows you to add badges, keyboard shortcuts, status indicators, and more.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content across all menu types.
+
### Show Menu only over Certain Columns
Say you want to show the Context Menu only when the user is over certain columns of the grid. For that, you could use the `commandShownOverColumnIds` (or `optionShownOverColumnIds`) array, by default these arrays are empty and when that is the case then the menu will be accessible from any columns. So if we want to have the Context Menu available only over the first 2 columns, we would have an array of those 2 column ids. For example, the following would show the Context Menu everywhere except the last 2 columns (priority, action) since they are not part of the array.
```ts
diff --git a/frameworks/slickgrid-react/docs/grid-functionalities/grid-menu.md b/frameworks/slickgrid-react/docs/grid-functionalities/grid-menu.md
index 586574a12a..9fb66f858e 100644
--- a/frameworks/slickgrid-react/docs/grid-functionalities/grid-menu.md
+++ b/frameworks/slickgrid-react/docs/grid-functionalities/grid-menu.md
@@ -73,6 +73,11 @@ const gridOptions = {
};
```
+#### Advanced: Dynamic Command List Builder
+For more advanced use cases where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list.
+
+See the main [Custom Menu Slots](../menu-slots.md) documentation for detailed `commandListBuilder` examples.
+
#### Events
There are multiple events/callback hooks which are accessible from the Grid Options
- `onBeforeMenuShow`
@@ -105,6 +110,11 @@ gridMenu: {
For more info on all the available properties of the custom commands, you can read refer to the doc written in the Grid Menu [implementation](https://github.com/6pac/SlickGrid/blob/master/controls/slick.gridmenu.js) itself.
+### Custom Menu Item Rendering
+To customize the appearance of menu items with custom HTML, badges, icons, or interactive elements, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content.
+
### How to change an icon of all default commands?
You can change any of the default command icon(s) by changing the `icon[X-command]`, for example, see below for the defaults.
```ts
diff --git a/frameworks/slickgrid-react/docs/grid-functionalities/header-menu-header-buttons.md b/frameworks/slickgrid-react/docs/grid-functionalities/header-menu-header-buttons.md
index 0bcf07b3ab..1d14f68ab3 100644
--- a/frameworks/slickgrid-react/docs/grid-functionalities/header-menu-header-buttons.md
+++ b/frameworks/slickgrid-react/docs/grid-functionalities/header-menu-header-buttons.md
@@ -20,7 +20,10 @@ The Header Menu also comes, by default, with a list of built-in custom commands
- Sort Descending (you can hide it with `hideSortCommands: true`)
- Hide Column (you can hide it with `hideColumnHideCommand: true`)
-This section is called Custom Commands because you can also customize this section with your own commands. To do that, you need to fill in 2 properties (an array of `headerMenuItems` that will go under each column definition and define `onCommand` callbacks) in your Grid Options. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
+This section is called Custom Commands because you can also customize this section with your own commands. You can do this in two ways: using static command items or using a dynamic command list builder.
+
+#### Static Command Items
+To add static commands, fill in an array of items in your column definition's `header.menu.commandItems`. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
```ts
const gridOptions = {
enableAutoResize: true,
@@ -51,6 +54,100 @@ const gridOptions = {
}
};
```
+
+#### Dynamic Command List Builder
+For advanced use cases where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list. This is executed **after** `commandItems` and is the **last call before rendering**.
+
+```ts
+const columnDefinitions = [
+ {
+ id: 'title',
+ name: 'Title',
+ field: 'title',
+ header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ // Append custom commands to the built-in sort/hide commands
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'freeze-column',
+ title: 'Freeze Column',
+ iconCssClass: 'mdi mdi-pin',
+ action: (e, args) => {
+ // Implement column freezing
+ console.log('Freeze column:', args.column.name);
+ }
+ }
+ ];
+ }
+ }
+ }
+ }
+];
+```
+
+**Example: Conditional commands based on column type**
+```ts
+header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ const column = this; // column context
+
+ // Add filtering option only for filterable columns
+ const extraCommands = [];
+ if (column.filterable !== false) {
+ extraCommands.push({
+ command: 'clear-filter',
+ title: 'Clear Filter',
+ iconCssClass: 'mdi mdi-filter-remove',
+ action: (e, args) => {
+ filterService.clearFilterByColumnId(args.column.id);
+ }
+ });
+ }
+
+ return [...builtInItems, ...extraCommands];
+ }
+ }
+}
+```
+
+**Example: Remove sort commands, keep only custom ones**
+```ts
+header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ // Filter out sort commands
+ const filtered = builtInItems.filter(item =>
+ item !== 'divider' &&
+ !item.command?.includes('sort')
+ );
+
+ // Add custom commands
+ return [
+ ...filtered,
+ {
+ command: 'custom-action',
+ title: 'Custom Action',
+ iconCssClass: 'mdi mdi-star',
+ action: (e, args) => alert('Custom: ' + args.column.name)
+ }
+ ];
+ }
+ }
+}
+```
+
+**When to use `commandListBuilder`:**
+- You want to append/prepend items to built-in commands
+- You need to filter or modify commands based on column properties
+- You want to customize the command list per column dynamically
+- You need full control over the final command list
+
+**Note:** Use `commandListBuilder` **instead of** `commandItems`, not both together.
+
#### Callback Hooks
There are 2 callback hooks which are accessible in the Grid Options
- `onBeforeMenuShow`
@@ -58,6 +155,11 @@ There are 2 callback hooks which are accessible in the Grid Options
For more info on all the available properties of the custom commands, you can read refer to the doc written in the Grid Menu [implementation](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/extensions/slickHeaderButtons.ts) itself.
+### Custom Menu Item Rendering
+To customize the appearance of menu items with custom HTML, badges, icons, or interactive elements, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content.
+
### How to change icon(s) of the default commands?
You can change any of the default command icon(s) by changing the `icon[X-command]`, for example, see below for the defaults.
```ts
diff --git a/frameworks/slickgrid-react/docs/menu-slots.md b/frameworks/slickgrid-react/docs/menu-slots.md
new file mode 100644
index 0000000000..14b9f9bb47
--- /dev/null
+++ b/frameworks/slickgrid-react/docs/menu-slots.md
@@ -0,0 +1,524 @@
+## Custom Menu Slots - Rendering
+
+All menu plugins (Header Menu, Cell Menu, Context Menu, Grid Menu) support **cross-framework compatible slot rendering** for custom content injection in menu items. This is achieved through the `slotRenderer` callback at the item level combined with an optional `defaultMenuItemRenderer` at the menu level.
+
+> **Note:** This documentation covers **how menu items are rendered** (visual presentation). If you need to **dynamically modify which commands appear** in the menu (filtering, sorting, adding/removing items), see the `commandListBuilder` callback documented in [Grid Menu](grid-functionalities/grid-menu.md), [Context Menu](grid-functionalities/context-menu.md), or [Header Menu](grid-functionalities/header-menu-header-buttons.md).
+
+### TypeScript Tip: Type Inference with commandListBuilder
+
+When using `commandListBuilder` to add custom menu items with slotRenderer callbacks, **cast the return value to the appropriate type** to enable proper type parameters in callbacks:
+
+```typescript
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ return [
+ ...builtInItems,
+ {
+ command: 'custom-action',
+ title: 'My Action',
+ slotRenderer: (cmdItem, args) => `
${cmdItem.title}
`,
+ }
+ ] as Array;
+ }
+}
+```
+
+Alternatively, if you only have built-in items and dividers, you can use the simpler cast:
+
+```typescript
+return [...] as Array;
+```
+
+### Core Concept
+
+Each menu item can define a `slotRenderer` callback function that receives the item and args, and returns either an HTML string or an HTMLElement. This single API works uniformly across all menu plugins.
+
+### Slot Renderer Callback
+
+```typescript
+slotRenderer?: (cmdItem: MenuItem, args: MenuCallbackArgs, event?: Event) => string | HTMLElement
+```
+
+- **cmdItem** - The menu cmdItem object containing command, title, iconCssClass, etc.
+- **args** - The callback args providing access to grid, column, dataContext, and other context
+- **event** - Optional DOM event passed during click handling (allows `stopPropagation()`)
+
+### Basic Example - HTML String Rendering
+
+```typescript
+const menuItem = {
+ command: 'custom-command',
+ title: 'Custom Action',
+ iconCssClass: 'mdi mdi-star',
+ // Return custom HTML string for the entire menu item
+ slotRenderer: () => `
+
+
+ Custom Action
+ NEW
+
+ `
+};
+```
+
+### Advanced Example - HTMLElement Objects
+
+```typescript
+// Create custom element with full DOM control
+const menuItem = {
+ command: 'notifications',
+ title: 'Notifications',
+ // Return HTMLElement for more control and event listeners
+ slotRenderer: (cmdItem, args) => {
+ const container = document.createElement('div');
+ container.style.display = 'flex';
+ container.style.alignItems = 'center';
+
+ const icon = document.createElement('i');
+ icon.className = 'mdi mdi-bell';
+ icon.style.marginRight = '8px';
+
+ const text = document.createElement('span');
+ text.textContent = cmdItem.title;
+
+ const badge = document.createElement('span');
+ badge.className = 'badge';
+ badge.textContent = '5';
+ badge.style.marginLeft = 'auto';
+
+ container.appendChild(icon);
+ container.appendChild(text);
+ container.appendChild(badge);
+
+ return container;
+ }
+};
+```
+
+### Default Menu-Level Renderer
+
+Set a `defaultMenuItemRenderer` at the menu option level to apply to all items (unless overridden by individual `slotRenderer`):
+
+```typescript
+const menuOption = {
+ // Apply this renderer to all menu items (can be overridden per item)
+ defaultMenuItemRenderer: (cmdItem, args) => {
+ return `
+
';
+ }
+ }
+}
+```
diff --git a/frameworks/slickgrid-react/docs/migrations/migration-to-10.x.md b/frameworks/slickgrid-react/docs/migrations/migration-to-10.x.md
index 407756ae80..0005d30687 100644
--- a/frameworks/slickgrid-react/docs/migrations/migration-to-10.x.md
+++ b/frameworks/slickgrid-react/docs/migrations/migration-to-10.x.md
@@ -1,4 +1,4 @@
-## Cleaner Code / Smaller Code ⚡
+## Simplification and Modernization ⚡
One of the biggest change of this release is to hide columns by using the `hidden` column property (now used by Column Picker, Grid Menu, etc...). Previously we were removing columns from the original columns array and we then called `setColumns()` to update the grid, but this meant that we had to keep references for all visible and non-visible columns. With this new release we now keep the full columns array at all time and instead we just change column(s) visibility via their `hidden` column properties by using `grid.updateColumnById('id', { hidden: true })` and finally we update the grid via `grid.updateColumns()`. What I'm trying to emphasis is that you should really stop using `grid.setColumns()` in v10+, and if you want to hide some columns when declaring the columns, then just update their `hidden` properties, see more details below...
@@ -85,11 +85,30 @@ gridOptions = {
};
```
-### External Resources are now auto-enabled
-
This change does not require any code update from the end user, but it is a change that you should probably be aware of nonetheless. The reason I decided to implement this is because I often forget myself to enable the associated flag and typically if you wanted to load the resource, then it's most probably because you also want it enabled. So for example, if your register `ExcelExportService` then the library will now auto-enable the resource with its associated flag (which in this case is `enableExcelExport:true`)... unless you already disabled the flag (or enabled) yourself, if so then the internal assignment will simply be skipped and yours will prevail. Also just to be clear, the list of auto-enabled external resources is rather small, it will auto-enable the following resources:
(ExcelExportService, PdfExportService, TextExportService, CompositeEditorComponent and RowDetailView).
+### Menu with Commands
+
+All menu plugins (Cell Menu, Context Menu, Header Menu and Grid Menu) now have a new `commandListBuilder: (items) => items` which now allow you to filter/sort and maybe override built-in commands. With this new feature in place, I'm deprecating all `hide...` properties and also `positionOrder` since you can now do that with the builder. You could also use the `hideCommands` which accepts an array of built-in command names. This well remove huge amount of `hide...` properties (over 30) that keeps increasing anytime a new built-in command gets added (in other words, this will simplify maintenance for both you and me).
+
+These are currently just deprecations in v10.x but it's strongly recommended to start using the `commandListBuilder` and/or `hideCommands` and move away from the deprecated properties which will be removed in v11.x. For example if we want to hide some built-in commands:
+
+```diff
+gridOptions = {
+ gridMenu: {
+- hideExportCsvCommand: true,
+- hideTogglePreHeaderCommand: true,
+
+// via command name(s)
++ hideCommands: ['export-csv', 'toggle-preheader'],
+
+// or via builder
++ commandListBuilder: (cmdItems) => [...cmdItems.filter(x => x !== 'divider' && x.command !== 'export-csv' && x.command !== 'toggle-preheader')]
+ }
+}
+```
+
---
{% hint style="note" %}
diff --git a/frameworks/slickgrid-react/tsconfig.json b/frameworks/slickgrid-react/tsconfig.json
index d6cc0e4af9..27be555f78 100644
--- a/frameworks/slickgrid-react/tsconfig.json
+++ b/frameworks/slickgrid-react/tsconfig.json
@@ -22,7 +22,6 @@
"skipLibCheck": true,
"sourceMap": true,
"newLine": "lf",
- "downlevelIteration": true,
"outDir": "dist"
},
"include": ["src"],
diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/cell-menu.md b/frameworks/slickgrid-vue/docs/column-functionalities/cell-menu.md
index 15a8b3d00f..4ff57137ed 100644
--- a/frameworks/slickgrid-vue/docs/column-functionalities/cell-menu.md
+++ b/frameworks/slickgrid-vue/docs/column-functionalities/cell-menu.md
@@ -94,7 +94,8 @@ So if you decide to use the `action` callback, then your code would look like th
##### with `action` callback
```ts
columnDefinitions.value = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
commandItems: [
{ command: 'command1', title: 'Command 1', action: (e, args) => console.log(args) },
@@ -111,7 +112,8 @@ The `onCommand` (or `onOptionSelected`) **must** be defined in the Grid Options
```ts
columnDefinitions.value = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
commandItems: [
{ command: 'command1', title: 'Command 1' },
@@ -140,6 +142,11 @@ gridOptions.value = {
};
```
+### Custom Menu Item Rendering
+For advanced customization of menu item appearance, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks to create custom HTML or HTMLElement content for your menu items. This allows you to add badges, keyboard shortcuts, status indicators, and more.
+
+See [Custom Menu Slots](../../menu-slots.md) for detailed examples and best practices on rendering custom menu item content across all menu types.
+
### Override Callback Methods
What if you want to dynamically disable or hide a Command/Option or even disable the entire menu in certain circumstances? For these cases, you would use the override callback methods, the method must return a `boolean`. The list of override available are the following
- `menuUsabilityOverride` returning false would make the Cell Menu unavailable to the user
@@ -150,12 +157,10 @@ What if you want to dynamically disable or hide a Command/Option or even disable
For example, say we want the Cell Menu to only be available on the first 20 rows of the grid, we could use the override this way
```ts
columnDefinitions.value = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
- menuUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
- return (dataContext.id < 21); // say we want to display the menu only from Task 0 to 20
- },
+ menuUsabilityOverride: (args) => args?.dataContext.id < 21, // say we want to display the menu only from Task 0 to 20
}
}
];
@@ -164,14 +169,15 @@ columnDefinitions.value = [
To give another example, with Options this time, we could say that we enable the `n/a` option only when the row is Completed. So we could do it this way
```ts
columnDefinitions.value = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
optionItems: [
{
option: 0, title: 'n/a', textCssClass: 'italic',
// only enable this option when the task is Not Completed
itemUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
}
},
@@ -188,7 +194,8 @@ columnDefinitions.value = [
It works exactly like the rest of the library when `enableTranslate` is set, all we have to do is to provide translations with the `Key` suffix, so for example without translations, we would use `title` and that would become `titleKey` with translations, that;'s easy enough. So for example, a list of Options could be defined as follow:
```ts
columnDefinitions.value = [
- { id: 'action', field: 'action', name: 'Action',
+ {
+ id: 'action', field: 'action', name: 'Action',
cellMenu: {
optionTitleKey: 'OPTIONS', // optionally pass a title to show over the Options
optionItems: [
diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/editors.md b/frameworks/slickgrid-vue/docs/column-functionalities/editors.md
index 78151d2c9c..511b3fe4c8 100644
--- a/frameworks/slickgrid-vue/docs/column-functionalities/editors.md
+++ b/frameworks/slickgrid-vue/docs/column-functionalities/editors.md
@@ -13,7 +13,7 @@
- [Collection Async Load](#collection-async-load)
- [Collection Label Prefix/Suffix](#collection-label-prefixsuffix)
- [Collection Label Render HTML](#collection-label-render-html)
- - [`multiple-select.js` Options](#multiple-selectjs-options)
+ - [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- [Editor Options](#editor-options)
- [Validators](#validators)
- [Custom Validator](#custom-validator)
@@ -256,7 +256,7 @@ function defineGrid() {
```
### Editor Options (`MultipleSelectOption` interface)
-All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select.js` library.
+All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select-vanilla` library.
```ts
editor: {
@@ -417,7 +417,7 @@ function defineGrid() {
collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }],
model: Editors.singleSelect,
elementOptions: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
}
@@ -498,8 +498,8 @@ So if we take all of these informations and we want to create our own Custom Edi
```ts
const myCustomTitleValidator: EditorValidator = (value: any, args: EditorArgs) => {
// you can get the Editor Args which can be helpful, e.g. we can get the Translate Service from it
- const grid = args && args.grid;
- const gridOptions = (grid && grid.getOptions) ? grid.getOptions() : {};
+ const grid = args.grid;
+ const gridOptions = grid.getOptions() : {};
const i18n = gridOptions.i18n;
if (value == null || value === undefined || !value.length) {
diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/editors/select-dropdown-editor.md b/frameworks/slickgrid-vue/docs/column-functionalities/editors/select-dropdown-editor.md
index ec1f631ff7..22d767d68a 100644
--- a/frameworks/slickgrid-vue/docs/column-functionalities/editors/select-dropdown-editor.md
+++ b/frameworks/slickgrid-vue/docs/column-functionalities/editors/select-dropdown-editor.md
@@ -6,11 +6,11 @@
- [Collection Label Prefix/Suffix](#collection-label-prefixsuffix)
- [Collection Label Render HTML](#collection-label-render-html)
- [Collection Change Watch](#collection-watch)
- - [`multiple-select.js` Options](#multiple-selectjs-options)
+ - [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- See the [Editors - Wiki](../Editors.md) for more general info about Editors (validators, event handlers, ...)
## Select Editors
-The library ships with two select editors: `singleSelectEditor` and the `multipleSelectEditor`. Both support the [multiple-select](https://github.com/ghiscoding/multiple-select-adapted/blob/master/src/multiple-select.js) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/collectionFormatter.ts).
+The library ships with two select editors: `singleSelectEditor` and the `multipleSelectEditor`. Both support the [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/collectionFormatter.ts).
We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla).
@@ -38,7 +38,7 @@ columnDefinitions.value = [
```
### Editor Options (`MultipleSelectOption` interface)
-All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select.js` library.
+All the available options that can be provided as editor `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your editor `options` to that interface to make sure that you use only valid options of the `multiple-select-vanilla` library.
```ts
editor: {
@@ -195,7 +195,7 @@ columnDefinitions.value = [
```
### `multiple-select-vanilla.js` Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your editor `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/lib` folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -221,7 +221,7 @@ columnDefinitions.value = [
collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }],
model: Editors.singleSelect,
elementOptions: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
}
diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/filters/custom-filter.md b/frameworks/slickgrid-vue/docs/column-functionalities/filters/custom-filter.md
index 63ec606e33..91f3c98070 100644
--- a/frameworks/slickgrid-vue/docs/column-functionalities/filters/custom-filter.md
+++ b/frameworks/slickgrid-vue/docs/column-functionalities/filters/custom-filter.md
@@ -12,7 +12,7 @@ You can also create your own Custom Filter with any html/css you want to use. Vu
- as mentioned in the description, only html/css and/or JS libraries are supported.
- this mainly mean that Vue templates (Views) are not supported (feel free to contribute).
- SlickGrid uses `table-cell` as CSS for it to display a consistent height for each rows (this keeps the same row height/line-height to always be the same).
- - all this to say that you might be in a situation were your filter shows in the back of the grid. The best approach to overcome this is to use a modal if you can or if the library support `append to body container`. For example, you can see that `multiple-select.js` support a `container` and is needed for the filter to work as can be seen in the [multipleSelectFilter.ts](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/filters/multipleSelectFilter.ts#L26)
+ - all this to say that you might be in a situation were your filter shows in the back of the grid. The best approach to overcome this is to use a modal if you can or if the library support `append to body container`. For example, you can see that `multiple-select-vanilla` support a `container` and is needed for the filter to work as can be seen in the [multipleSelectFilter.ts](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/filters/multipleSelectFilter.ts#L26)
### How to use Custom Filter?
1. You first need to create a `class` using the [Filter interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/models/filter.interface.ts). Make sure to create all necessary public properties and functions.
diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/filters/select-filter.md b/frameworks/slickgrid-vue/docs/column-functionalities/filters/select-filter.md
index f0955109e0..fbc65424de 100644
--- a/frameworks/slickgrid-vue/docs/column-functionalities/filters/select-filter.md
+++ b/frameworks/slickgrid-vue/docs/column-functionalities/filters/select-filter.md
@@ -16,7 +16,7 @@
- [Collection Async Load](#collection-async-load)
- [Collection Lazy Load](#collection-lazy-load)
- [Collection Watch](#collection-watch)
-- [`multiple-select.js` Options](#multiple-selectjs-options)
+- [`multiple-select-vanilla` Options](#multiple-selectjs-options)
- [Filter Options (`MultipleSelectOption` interface)](#filter-options-multipleselectoption-interface)
- [Display shorter selected label text](#display-shorter-selected-label-text)
- [Query against a different field](#query-against-another-field-property)
@@ -36,7 +36,7 @@ Multiple Select (dropdown) filter is useful when we want to filter the grid 1 or
We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla).
#### Note
-For this filter to work you will need to add [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) to your project. This is a customized version of the original (thought all the original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options). Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/dist/lib` folder. This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original)
+For this filter to work you will need to add [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) to your project. This is a customized version of the original (thought all the original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options). Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/dist/lib` folder. This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original)
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
- `okButtonText` was also added for locale (i18n)
- `offsetLeft` option was added to make it possible to offset the dropdown. By default it is set to 0 and is aligned to the left of the select element. This option is particularly helpful when used as the last right column, not to fall off the screen.
@@ -590,7 +590,7 @@ columnDefinitions.value = [
```
### Filter Options (`MultipleSelectOption` interface)
-All the available options that can be provided as filter `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your filter `options` to that interface to make sure that you use only valid options of the `multiple-select.js` library.
+All the available options that can be provided as filter `options` to your column definitions can be found under this [MultipleSelectOption](https://github.com/ghiscoding/multiple-select-vanilla/blob/main/packages/multiple-select-vanilla/src/models/multipleSelectOption.interface.ts) interface and you should cast your filter `options` to that interface to make sure that you use only valid options of the `multiple-select-vanilla` library.
```ts
filter: {
@@ -613,8 +613,8 @@ gridOptions.value = {
}
```
-### Multiple-select.js Options
-You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your filter `options` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options).
+### multiple-select-vanilla Options
+You can use any options from [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your filter `options` property. However please note that this is a customized version of the original (all original [lib options](https://ghiscoding.github.io/multiple-select-vanilla/) are available so you can still consult the original site for all options).
Couple of small options were added to suit SlickGrid-Universal needs, which is why we are using a fork [ghiscoding/multiple-select-modified](https://github.com/ghiscoding/multiple-select-modified) folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following:
- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
@@ -643,7 +643,7 @@ columnDefinitions.value = [
model: Filters.singleSelect,
// previously known as `filterOptions` for < 9.0
options: {
- // add any multiple-select.js options (from original or custom version)
+ // add any multiple-select-vanilla options (from original or custom version)
autoAdjustDropPosition: false, // by default set to True, but you can disable it
position: 'top'
} as MultipleSelectOption
diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/composite-editor-modal.md b/frameworks/slickgrid-vue/docs/grid-functionalities/composite-editor-modal.md
index 8a8e367454..94aa193282 100644
--- a/frameworks/slickgrid-vue/docs/grid-functionalities/composite-editor-modal.md
+++ b/frameworks/slickgrid-vue/docs/grid-functionalities/composite-editor-modal.md
@@ -492,7 +492,7 @@ function handleOnCompositeEditorChange(event) {
// you can also change some editor options
// not all Editors supports this functionality, so far only these Editors are supported: AutoComplete, Date, Single/Multiple Select
if (columnDef.id === 'completed') {
- compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select.js, show filter in dropdown
+ compositeEditorInstance.changeFormEditorOption('percentComplete', 'filter', true); // multiple-select-vanilla, show filter in dropdown
compositeEditorInstance.changeFormEditorOption('product', 'minLength', 3); // autocomplete, change minLength char to type
compositeEditorInstance.changeFormEditorOption('finish', 'displayDateMin', 'today');
}
diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/context-menu.md b/frameworks/slickgrid-vue/docs/grid-functionalities/context-menu.md
index c03cbc04c5..638528bf65 100644
--- a/frameworks/slickgrid-vue/docs/grid-functionalities/context-menu.md
+++ b/frameworks/slickgrid-vue/docs/grid-functionalities/context-menu.md
@@ -23,7 +23,7 @@ This extensions is wrapped around the new SlickGrid Plugin **SlickContextMenu**
### Default Usage
Technically, the Context Menu is enabled by default (copy, export) and so you don't have anything to do to enjoy it (you could disable it at any time). However, if you want to customize the content of the Context Menu, then continue reading. You can customize the menu with 2 different lists, Commands and/or Options, they can be used separately or at the same time. Also note that even though the code shown below makes a separation between the Commands and Options, you can mix them in the same Context Menu.
-#### with Commands
+#### with Commands (Static)
```ts
gridOptions.value = {
@@ -63,6 +63,98 @@ gridOptions.value = {
};
```
+#### with Commands (Dynamic Builder)
+For advanced scenarios where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list.
+
+```ts
+this.gridOptions = {
+ enableContextMenu: true,
+ contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ // Example: Add custom commands after built-in ones
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'delete-row',
+ title: 'Delete Row',
+ iconCssClass: 'mdi mdi-delete text-danger',
+ action: (e, args) => {
+ if (confirm(`Delete row ${args.dataContext.id}?`)) {
+ this.gridService.deleteItem(args.dataContext);
+ }
+ }
+ },
+ {
+ command: 'duplicate-row',
+ title: 'Duplicate Row',
+ iconCssClass: 'mdi mdi-content-duplicate',
+ action: (e, args) => {
+ const newItem = { ...args.dataContext, id: this.generateNewId() };
+ this.gridService.addItem(newItem);
+ }
+ }
+ ];
+ },
+ onCommand: (e, args) => {
+ // Handle commands here if not using action callbacks
+ console.log('Command:', args.command);
+ }
+ }
+};
+```
+
+**Example: Filter commands based on row data**
+```ts
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ // You can't access row data here, but you can filter/modify built-in items
+ // Use itemUsabilityOverride or itemVisibilityOverride for row-specific logic
+
+ // Only show export commands
+ return builtInItems.filter(item =>
+ item === 'divider' || item.command?.includes('export')
+ );
+ }
+}
+```
+
+**Example: Sort and reorganize commands**
+```ts
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ const customFirst = [
+ {
+ command: 'edit',
+ title: 'Edit Row',
+ iconCssClass: 'mdi mdi-pencil',
+ positionOrder: 0
+ }
+ ];
+
+ // Sort built-in commands by positionOrder
+ const sorted = [...builtInItems].sort((a, b) => {
+ if (a === 'divider' || b === 'divider') return 0;
+ return (a.positionOrder || 50) - (b.positionOrder || 50);
+ });
+
+ return [...customFirst, 'divider', ...sorted];
+ }
+}
+```
+
+**When to use `commandListBuilder` vs `commandItems`:**
+- Use `commandItems` for static command lists
+- Use `commandListBuilder` when you need to:
+ - Append/prepend to built-in commands
+ - Filter or modify commands dynamically
+ - Sort or reorder the final command list
+ - Have full control over what gets rendered
+
+**Note:** Typically use `commandListBuilder` **instead of** `commandItems`, not both together.
+
+See the main [Custom Menu Slots](../menu-slots.md) documentation for detailed `commandListBuilder` examples.
+
#### with Options
That is when you want to define a list of Options (only 1 list) that the user can choose from and once is selected we would do something (for example change the value of a cell in the grid).
@@ -79,7 +171,7 @@ gridOptions.value = {
// subscribe to Context Menu onOptionSelected event (or use the "action" callback on each option)
onOptionSelected: (e, args) => {
// change Priority
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
if (dataContext && dataContext.hasOwnProperty('priority')) {
dataContext.priority = args.item.option;
sgb.gridService.updateItem(dataContext);
@@ -139,7 +231,7 @@ For example, say we want the Context Menu to only be available on the first 20 r
```ts
contextMenu: {
menuUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return (dataContext.id < 21); // say we want to display the menu only from Task 0 to 20
},
},
@@ -153,7 +245,7 @@ contextMenu: {
option: 0, title: 'n/a', textCssClass: 'italic',
// only enable this option when the task is Not Completed
itemUsabilityOverride: (args) => {
- const dataContext = args && args.dataContext;
+ const dataContext = args?.dataContext;
return !dataContext.completed;
},
},
@@ -177,6 +269,11 @@ contextMenu: {
}
```
+### Custom Menu Item Rendering
+For advanced customization of menu item appearance, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks to create custom HTML or HTMLElement content for your menu items. This allows you to add badges, keyboard shortcuts, status indicators, and more.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content across all menu types.
+
### Show Menu only over Certain Columns
Say you want to show the Context Menu only when the user is over certain columns of the grid. For that, you could use the `commandShownOverColumnIds` (or `optionShownOverColumnIds`) array, by default these arrays are empty and when that is the case then the menu will be accessible from any columns. So if we want to have the Context Menu available only over the first 2 columns, we would have an array of those 2 column ids. For example, the following would show the Context Menu everywhere except the last 2 columns (priority, action) since they are not part of the array.
```ts
diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/grid-menu.md b/frameworks/slickgrid-vue/docs/grid-functionalities/grid-menu.md
index d89c54482b..5a5485a1d1 100644
--- a/frameworks/slickgrid-vue/docs/grid-functionalities/grid-menu.md
+++ b/frameworks/slickgrid-vue/docs/grid-functionalities/grid-menu.md
@@ -74,6 +74,11 @@ gridOptions.value = {
};
```
+#### Advanced: Dynamic Command List Builder
+For more advanced use cases where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list.
+
+See the main [Custom Menu Slots](../menu-slots.md) documentation for detailed `commandListBuilder` examples.
+
#### Events
There are multiple events/callback hooks which are accessible from the Grid Options
- `onBeforeMenuShow`
@@ -106,6 +111,11 @@ gridMenu: {
For more info on all the available properties of the custom commands, you can read refer to the doc written in the Grid Menu [implementation](https://github.com/6pac/SlickGrid/blob/master/controls/slick.gridmenu.js) itself.
+### Custom Menu Item Rendering
+To customize the appearance of menu items with custom HTML, badges, icons, or interactive elements, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content.
+
### How to change an icon of all default commands?
You can change any of the default command icon(s) by changing the `icon[X-command]`, for example, see below for the defaults.
```ts
diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/header-menu-header-buttons.md b/frameworks/slickgrid-vue/docs/grid-functionalities/header-menu-header-buttons.md
index db633290d7..09bafcdb3c 100644
--- a/frameworks/slickgrid-vue/docs/grid-functionalities/header-menu-header-buttons.md
+++ b/frameworks/slickgrid-vue/docs/grid-functionalities/header-menu-header-buttons.md
@@ -20,7 +20,10 @@ The Header Menu also comes, by default, with a list of built-in custom commands
- Sort Descending (you can hide it with `hideSortCommands: true`)
- Hide Column (you can hide it with `hideColumnHideCommand: true`)
-This section is called Custom Commands because you can also customize this section with your own commands. To do that, you need to fill in 2 properties (an array of `headerMenuItems` that will go under each column definition and define `onCommand` callbacks) in your Grid Options. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
+This section is called Custom Commands because you can also customize this section with your own commands. You can do this in two ways: using static command items or using a dynamic command list builder.
+
+#### Static Command Items
+To add static commands, fill in an array of items in your column definition's `header.menu.commandItems`. For example, `Slickgrid-Universal` is configured by default with these settings (you can overwrite any one of them):
```ts
gridOptions.value = {
enableAutoResize: true,
@@ -51,6 +54,98 @@ gridOptions.value = {
}
};
```
+#### Dynamic Command List Builder
+For advanced use cases where you need to dynamically build the command list, use `commandListBuilder`. This callback receives the built-in commands and allows you to filter, sort, or modify the list before it's rendered in the UI, giving you full control over the final command list. This is executed **after** `commandItems` and is the **last call before rendering**.
+
+```ts
+columnDefinitions.value = [
+ {
+ id: 'title',
+ name: 'Title',
+ field: 'title',
+ header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ // Append custom commands to the built-in sort/hide commands
+ return [
+ ...builtInItems,
+ 'divider',
+ {
+ command: 'freeze-column',
+ title: 'Freeze Column',
+ iconCssClass: 'mdi mdi-pin',
+ action: (e, args) => {
+ // Implement column freezing
+ console.log('Freeze column:', args.column.name);
+ }
+ }
+ ];
+ }
+ }
+ }
+ }
+];
+```
+
+**Example: Conditional commands based on column type**
+```ts
+header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ const column = this; // column context
+
+ // Add filtering option only for filterable columns
+ const extraCommands = [];
+ if (column.filterable !== false) {
+ extraCommands.push({
+ command: 'clear-filter',
+ title: 'Clear Filter',
+ iconCssClass: 'mdi mdi-filter-remove',
+ action: (e, args) => {
+ filterService.clearFilterByColumnId(args.column.id);
+ }
+ });
+ }
+
+ return [...builtInItems, ...extraCommands];
+ }
+ }
+}
+```
+
+**Example: Remove sort commands, keep only custom ones**
+```ts
+header: {
+ menu: {
+ commandListBuilder: (builtInItems) => {
+ // Filter out sort commands
+ const filtered = builtInItems.filter(item =>
+ item !== 'divider' &&
+ !item.command?.includes('sort')
+ );
+
+ // Add custom commands
+ return [
+ ...filtered,
+ {
+ command: 'custom-action',
+ title: 'Custom Action',
+ iconCssClass: 'mdi mdi-star',
+ action: (e, args) => alert('Custom: ' + args.column.name)
+ }
+ ];
+ }
+ }
+}
+```
+
+**When to use `commandListBuilder`:**
+- You want to append/prepend items to built-in commands
+- You need to filter or modify commands based on column properties
+- You want to customize the command list per column dynamically
+- You need full control over the final command list
+
+**Note:** Use `commandListBuilder` **instead of** `commandItems`, not both together.
#### Callback Hooks
There are 2 callback hooks which are accessible in the Grid Options
- `onBeforeMenuShow`
@@ -58,6 +153,11 @@ There are 2 callback hooks which are accessible in the Grid Options
For more info on all the available properties of the custom commands, you can read refer to the doc written in the Grid Menu [implementation](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/extensions/slickHeaderButtons.ts) itself.
+### Custom Menu Item Rendering
+To customize the appearance of menu items with custom HTML, badges, icons, or interactive elements, you can use the `slotRenderer` or `defaultMenuItemRenderer` callbacks.
+
+See [Custom Menu Slots](../menu-slots.md) for detailed examples and best practices on rendering custom menu item content.
+
### How to change icon(s) of the default commands?
You can change any of the default command icon(s) by changing the `icon[X-command]`, for example, see below for the defaults.
```ts
diff --git a/frameworks/slickgrid-vue/docs/menu-slots.md b/frameworks/slickgrid-vue/docs/menu-slots.md
new file mode 100644
index 0000000000..14b9f9bb47
--- /dev/null
+++ b/frameworks/slickgrid-vue/docs/menu-slots.md
@@ -0,0 +1,524 @@
+## Custom Menu Slots - Rendering
+
+All menu plugins (Header Menu, Cell Menu, Context Menu, Grid Menu) support **cross-framework compatible slot rendering** for custom content injection in menu items. This is achieved through the `slotRenderer` callback at the item level combined with an optional `defaultMenuItemRenderer` at the menu level.
+
+> **Note:** This documentation covers **how menu items are rendered** (visual presentation). If you need to **dynamically modify which commands appear** in the menu (filtering, sorting, adding/removing items), see the `commandListBuilder` callback documented in [Grid Menu](grid-functionalities/grid-menu.md), [Context Menu](grid-functionalities/context-menu.md), or [Header Menu](grid-functionalities/header-menu-header-buttons.md).
+
+### TypeScript Tip: Type Inference with commandListBuilder
+
+When using `commandListBuilder` to add custom menu items with slotRenderer callbacks, **cast the return value to the appropriate type** to enable proper type parameters in callbacks:
+
+```typescript
+contextMenu: {
+ commandListBuilder: (builtInItems) => {
+ return [
+ ...builtInItems,
+ {
+ command: 'custom-action',
+ title: 'My Action',
+ slotRenderer: (cmdItem, args) => `
${cmdItem.title}
`,
+ }
+ ] as Array;
+ }
+}
+```
+
+Alternatively, if you only have built-in items and dividers, you can use the simpler cast:
+
+```typescript
+return [...] as Array;
+```
+
+### Core Concept
+
+Each menu item can define a `slotRenderer` callback function that receives the item and args, and returns either an HTML string or an HTMLElement. This single API works uniformly across all menu plugins.
+
+### Slot Renderer Callback
+
+```typescript
+slotRenderer?: (cmdItem: MenuItem, args: MenuCallbackArgs, event?: Event) => string | HTMLElement
+```
+
+- **cmdItem** - The menu cmdItem object containing command, title, iconCssClass, etc.
+- **args** - The callback args providing access to grid, column, dataContext, and other context
+- **event** - Optional DOM event passed during click handling (allows `stopPropagation()`)
+
+### Basic Example - HTML String Rendering
+
+```typescript
+const menuItem = {
+ command: 'custom-command',
+ title: 'Custom Action',
+ iconCssClass: 'mdi mdi-star',
+ // Return custom HTML string for the entire menu item
+ slotRenderer: () => `
+
+
+ Custom Action
+ NEW
+
+ `
+};
+```
+
+### Advanced Example - HTMLElement Objects
+
+```typescript
+// Create custom element with full DOM control
+const menuItem = {
+ command: 'notifications',
+ title: 'Notifications',
+ // Return HTMLElement for more control and event listeners
+ slotRenderer: (cmdItem, args) => {
+ const container = document.createElement('div');
+ container.style.display = 'flex';
+ container.style.alignItems = 'center';
+
+ const icon = document.createElement('i');
+ icon.className = 'mdi mdi-bell';
+ icon.style.marginRight = '8px';
+
+ const text = document.createElement('span');
+ text.textContent = cmdItem.title;
+
+ const badge = document.createElement('span');
+ badge.className = 'badge';
+ badge.textContent = '5';
+ badge.style.marginLeft = 'auto';
+
+ container.appendChild(icon);
+ container.appendChild(text);
+ container.appendChild(badge);
+
+ return container;
+ }
+};
+```
+
+### Default Menu-Level Renderer
+
+Set a `defaultMenuItemRenderer` at the menu option level to apply to all items (unless overridden by individual `slotRenderer`):
+
+```typescript
+const menuOption = {
+ // Apply this renderer to all menu items (can be overridden per item)
+ defaultMenuItemRenderer: (cmdItem, args) => {
+ return `
+