add conetext menu

This commit is contained in:
2026-05-07 22:20:57 +02:00
parent b0002fafd0
commit e29b4c5e58
3 changed files with 427 additions and 36 deletions

View File

@@ -3516,6 +3516,9 @@ function _normaliseQwertyAzerty(keyboardEvent) {
} }
return code; return code;
} }
function _isPromise(fn) {
return typeof fn.then === "function";
}
function _wrapInterval(action, timeout) { function _wrapInterval(action, timeout) {
return new AgPromise((resolve) => { return new AgPromise((resolve) => {
resolve(window.setInterval(action, timeout)); resolve(window.setInterval(action, timeout));
@@ -56519,6 +56522,174 @@ var AgMenuList = class extends AgTabGuardComp {
super.destroy(); super.destroy();
} }
}; };
var CSS_MENU = "ag-menu";
var CSS_CONTEXT_MENU_LOADING_ICON = "ag-context-menu-loading-icon";
var AgContextMenuService = class extends AgBeanStub {
constructor(params) {
super();
this.params = params;
this.destroyLoadingSpinner = null;
this.lastPromise = 0;
}
hideActiveMenu() {
this.destroyBean(this.activeMenu);
}
showMenu(menuActionParams, mouseEvent, anchorToElement) {
const { getMenuItems, shouldBlockMenuOpen: shouldBlockMenu } = this.params;
const menuItems = getMenuItems(menuActionParams, mouseEvent);
if (_isPromise(menuItems)) {
const currentPromise = this.lastPromise + 1;
this.lastPromise = currentPromise;
if (!this.destroyLoadingSpinner) {
this.createLoadingIcon(mouseEvent);
}
menuItems.then((menuItems2) => {
if (this.lastPromise !== currentPromise) {
return;
}
const { target } = mouseEvent;
const isFromFakeEvent = !target;
const shouldShowMenu = menuItems2?.length && (isFromFakeEvent || _isVisible(target)) && !shouldBlockMenu?.();
if (shouldShowMenu) {
this.createContextMenu({ menuItems: menuItems2, menuActionParams, mouseEvent, anchorToElement });
}
this.destroyLoadingSpinner?.();
});
return true;
}
if (!menuItems?.length) {
return false;
}
this.createContextMenu({ menuItems, menuActionParams, mouseEvent, anchorToElement });
return true;
}
createLoadingIcon(mouseEvent) {
const { beans } = this;
const translate = this.getLocaleTextFunc();
const loadingIcon = beans.iconSvc.createIconNoSpan("loadingMenuItems");
const wrapperEl = _createAgElement({ tag: "div", cls: CSS_CONTEXT_MENU_LOADING_ICON });
wrapperEl.appendChild(loadingIcon);
const rootNode = _getRootNode(beans);
const targetEl = _getPageBody(beans);
if (!targetEl) {
return;
}
targetEl.appendChild(wrapperEl);
beans.ariaAnnounce?.announceValue(translate("ariaLabelLoadingContextMenu", "Loading Context Menu"), "contextmenu");
beans.environment.applyThemeClasses(wrapperEl);
_anchorElementToMouseMoveEvent(wrapperEl, mouseEvent, beans);
const mouseMoveCallback = (e) => {
_anchorElementToMouseMoveEvent(wrapperEl, e, beans);
};
rootNode.addEventListener("mousemove", mouseMoveCallback);
this.destroyLoadingSpinner = () => {
rootNode.removeEventListener("mousemove", mouseMoveCallback);
wrapperEl.remove();
this.destroyLoadingSpinner = null;
};
}
createContextMenu(params) {
const {
mapMenuItems,
menuItemCallbacks,
beforeMenuOpen,
onMenuClose,
afterMenuDestroyed,
onVisibleChanged,
onMenuOpen
} = this.params;
const { menuItems, menuActionParams, mouseEvent, anchorToElement } = params;
const popupSvc = this.beans.popupSvc;
const getMenuItems = mapMenuItems ? (getGui) => mapMenuItems(menuItems, menuActionParams, getGui) : () => menuItems;
const menu = new ContextMenu(getMenuItems, menuActionParams, menuItemCallbacks);
this.createBean(menu);
const eMenuGui = menu.getGui();
beforeMenuOpen?.(menuActionParams);
const positionParams = {
additionalParams: menuItemCallbacks.getPostProcessPopupParams(menuActionParams),
type: "contextMenu",
mouseEvent,
ePopup: eMenuGui,
nudgeY: 1
};
const translate = this.getLocaleTextFunc();
const addPopupRes = popupSvc?.addPopup({
modal: true,
eChild: eMenuGui,
closeOnEsc: true,
closedCallback: (e) => {
menuItemCallbacks.preserveRangesWhile(this.beans, () => {
onMenuClose?.();
this.destroyBean(menu);
afterMenuDestroyed?.();
onVisibleChanged?.(false, e === undefined ? "api" : "ui");
});
},
click: mouseEvent,
positionCallback: () => {
const isRtl = this.gos.get("enableRtl");
popupSvc?.positionPopupUnderMouseEvent({
...positionParams,
nudgeX: isRtl ? (eMenuGui.offsetWidth + 1) * -1 : 1
});
},
anchorToElement,
ariaLabel: translate("ariaLabelContextMenu", "Context Menu")
});
if (addPopupRes) {
onMenuOpen?.();
menu.afterGuiAttached({ container: "contextMenu", hidePopup: addPopupRes.hideFunc });
}
if (this.activeMenu) {
this.hideActiveMenu();
}
this.activeMenu = menu;
menu.addEventListener("destroyed", () => {
if (this.activeMenu === menu) {
this.activeMenu = null;
}
});
if (addPopupRes) {
menu.addEventListener("closeMenu", (e) => addPopupRes.hideFunc({
mouseEvent: e.mouseEvent ?? undefined,
keyboardEvent: e.keyboardEvent ?? undefined,
forceHide: true
}));
}
const isApi = mouseEvent && mouseEvent instanceof MouseEvent && mouseEvent.type === "mousedown";
onVisibleChanged?.(true, isApi ? "api" : "ui");
}
destroy() {
this.destroyLoadingSpinner?.();
super.destroy();
}
};
var ContextMenu = class extends AgComponentStub {
constructor(getMenuItems, menuActionParams, callbacks) {
super({ tag: "div", cls: CSS_MENU, role: "presentation" });
this.getMenuItems = getMenuItems;
this.menuActionParams = menuActionParams;
this.callbacks = callbacks;
this.menuList = null;
}
postConstruct() {
const menuList = this.createManagedBean(new AgMenuList(0, this.menuActionParams, this.callbacks));
const menuItemsMapped = this.getMenuItems(() => this.getGui());
menuList.addMenuItems(menuItemsMapped);
this.appendChild(menuList);
this.menuList = menuList;
menuList.addEventListener("closeMenu", (e) => this.dispatchLocalEvent(e));
}
afterGuiAttached({ hidePopup }) {
if (hidePopup) {
this.addDestroyFunc(hidePopup);
}
const menuList = this.menuList;
if (menuList) {
this.callbacks.preserveRangesWhile(this.beans, () => _focusInto(menuList.getGui()));
}
}
};
var AgMenuItemRenderer = class extends AgComponentStub { var AgMenuItemRenderer = class extends AgComponentStub {
constructor(callbacks) { constructor(callbacks) {
super({ tag: "div" }); super({ tag: "div" });
@@ -63630,6 +63801,193 @@ var ColumnMenuFactory = class extends BeanStub {
return result; return result;
} }
}; };
var CSS_CONTEXT_MENU_OPEN = "ag-context-menu-open";
var ContextMenuService = class extends BeanStub {
constructor() {
super(...arguments);
this.beanName = "contextMenuSvc";
this.focusedCell = null;
}
postConstruct() {
this.menu = this.createManagedBean(new AgContextMenuService({
menuItemCallbacks: MENU_ITEM_CALLBACKS,
getMenuItems: this.getMenuItems.bind(this),
mapMenuItems: this.mapWithStockItems.bind(this),
beforeMenuOpen: this.beforeMenuOpen.bind(this),
onMenuOpen: this.onMenuOpen.bind(this),
onMenuClose: this.onMenuClose.bind(this),
afterMenuDestroyed: this.afterMenuDestroyed.bind(this),
onVisibleChanged: this.dispatchVisibleChangedEvent.bind(this),
shouldBlockMenuOpen: () => !!this.beans.overlays?.exclusive
}));
}
hideActiveMenu() {
this.menu.hideActiveMenu();
}
getMenuItems(menuActionParams, mouseEvent) {
const { column, node, value } = menuActionParams;
const defaultMenuOptions = [];
const { clipboardSvc, chartSvc, csvCreator, excelCreator, colModel, rangeSvc, gos } = this.beans;
if (_exists(node) && clipboardSvc) {
if (column) {
if (!gos.get("suppressCutToClipboard")) {
defaultMenuOptions.push("cut");
}
defaultMenuOptions.push("copy", "copyWithHeaders", "copyWithGroupHeaders", "paste", "separator");
}
}
if (gos.get("enableCharts") && chartSvc) {
if (colModel.isPivotMode()) {
defaultMenuOptions.push("pivotChart");
}
if (rangeSvc && !rangeSvc.isEmpty()) {
defaultMenuOptions.push("chartRange");
}
}
if (_exists(node)) {
const enableRowPinning = gos.get("enableRowPinning");
const isRowPinnable = gos.get("isRowPinnable");
if (enableRowPinning) {
const isGroupTotalRow = node.level > -1 && node.footer;
const isGrandTotalRow = node.level === -1 && node.footer;
const grandTotalRow = _getGrandTotalRow(gos);
const isGrandTotalRowFixed = grandTotalRow === "pinnedBottom" || grandTotalRow === "pinnedTop";
if (isGrandTotalRow && !isGrandTotalRowFixed || !isGrandTotalRow && !isGroupTotalRow) {
const pinnable = isRowPinnable?.(node) ?? true;
if (pinnable) {
defaultMenuOptions.push("pinRowSubMenu");
}
}
}
const suppressExcel = gos.get("suppressExcelExport") || !excelCreator;
const suppressCsv = gos.get("suppressCsvExport") || !csvCreator;
const onIPad = _isIOSUserAgent();
const anyExport = !onIPad && (!suppressExcel || !suppressCsv);
if (anyExport) {
defaultMenuOptions.push("export");
}
}
const defaultItems = defaultMenuOptions.length ? defaultMenuOptions : undefined;
const columnContextMenuItems = column?.getColDef().contextMenuItems;
if (Array.isArray(columnContextMenuItems)) {
return columnContextMenuItems;
}
if (typeof columnContextMenuItems === "function") {
return columnContextMenuItems(_addGridCommonParams(gos, {
column,
node,
value,
defaultItems,
event: mouseEvent
}));
}
const userFunc = gos.getCallback("getContextMenuItems");
return userFunc?.({ column, node, value, defaultItems, event: mouseEvent }) ?? defaultMenuOptions;
}
getContextMenuPosition(rowNode, column) {
const rowCtrl = this.getRowCtrl(rowNode);
const eGui = this.getCellGui(rowCtrl, column);
if (!eGui) {
return { x: 0, y: rowCtrl?.getRowYPosition() ?? 0 };
}
const rect = eGui.getBoundingClientRect();
return {
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2
};
}
showContextMenu(params) {
const rowNode = params.rowNode ?? null;
const column = params.column ?? null;
let { anchorToElement, value, source } = params;
if (rowNode && column && value == null) {
value = this.beans.valueSvc.getValueForDisplay({ column, node: rowNode, from: "edit" }).value;
}
if (anchorToElement == null) {
anchorToElement = this.getContextMenuAnchorElement(rowNode, column);
}
this.beans.menuUtils.onContextMenu({
mouseEvent: params.mouseEvent ?? null,
touchEvent: params.touchEvent ?? null,
showMenuCallback: (eventOrTouch) => this.menu.showMenu({ node: rowNode, column, value }, eventOrTouch, anchorToElement),
source
});
}
handleContextMenuMouseEvent(mouseEvent, touchEvent, rowCtrl, cellCtrl) {
const rowNode = cellCtrl?.rowNode ?? rowCtrl?.rowNode ?? null;
const column = cellCtrl?.column ?? rowCtrl?.findFullWidthInfoForEvent(mouseEvent || touchEvent)?.column ?? null;
const { valueSvc, ctrlsSvc } = this.beans;
const value = column ? valueSvc.getValue(column, rowNode, "edit") : null;
const gridBodyCon = ctrlsSvc.getGridBodyCtrl();
const anchorToElement = cellCtrl ? cellCtrl.eGui : gridBodyCon.eGridBody;
this.showContextMenu({
mouseEvent,
touchEvent,
rowNode,
column,
value,
anchorToElement,
source: "ui"
});
}
beforeMenuOpen(menuActionParams) {
if (!menuActionParams.column) {
this.beans.focusSvc.clearFocusedCell();
}
}
onMenuOpen() {
const { ctrlsSvc, focusSvc } = this.beans;
ctrlsSvc.getGridBodyCtrl().eGridBody.classList.add(CSS_CONTEXT_MENU_OPEN);
this.focusedCell = focusSvc.getFocusedCell();
}
onMenuClose() {
this.beans.ctrlsSvc.getGridBodyCtrl().eGridBody.classList.remove(CSS_CONTEXT_MENU_OPEN);
}
afterMenuDestroyed() {
const { beans, focusedCell } = this;
_attemptToRestoreCellFocus(beans, focusedCell);
}
dispatchVisibleChangedEvent(visible, source) {
this.eventSvc.dispatchEvent({
type: "contextMenuVisibleChanged",
visible,
source
});
}
getRowCtrl(rowNode) {
const { rowIndex, rowPinned } = rowNode || {};
if (rowIndex == null) {
return;
}
return this.beans.rowRenderer.getRowByPosition({ rowIndex, rowPinned }) || undefined;
}
getCellGui(rowCtrl, column) {
if (!rowCtrl || !column) {
return;
}
const cellCtrl = rowCtrl.getCellCtrl(column);
return cellCtrl?.eGui || undefined;
}
getContextMenuAnchorElement(rowNode, column) {
const gridBodyEl = this.beans.ctrlsSvc.getGridBodyCtrl().eGridBody;
const rowCtrl = this.getRowCtrl(rowNode);
if (!rowCtrl) {
return gridBodyEl;
}
const cellGui = this.getCellGui(rowCtrl, column);
if (cellGui) {
return cellGui;
}
if (rowCtrl.isFullWidth()) {
return rowCtrl.getFullWidthElement();
}
return gridBodyEl;
}
mapWithStockItems(menuItems, menuActionParams, getGui) {
const { column, node } = menuActionParams;
return this.beans.menuItemMapper.mapWithStockItems(menuItems, column, node, getGui, "contextMenu");
}
};
var TAB_FILTER = "filterMenuTab"; var TAB_FILTER = "filterMenuTab";
var TAB_GENERAL = "generalMenuTab"; var TAB_GENERAL = "generalMenuTab";
var TAB_COLUMNS = "columnsMenuTab"; var TAB_COLUMNS = "columnsMenuTab";
@@ -64011,6 +64369,27 @@ var ColumnContextMenu = class extends Component {
_focusInto(this.mainMenuList.getGui()); _focusInto(this.mainMenuList.getGui());
} }
}; };
function showContextMenu(beans, params) {
const { contextMenuSvc } = beans;
if (!contextMenuSvc) {
return;
}
const { rowNode, column, value, x, y } = params || {};
let { x: clientX, y: clientY } = contextMenuSvc.getContextMenuPosition(rowNode, column);
if (x != null) {
clientX = x;
}
if (y != null) {
clientY = y;
}
contextMenuSvc.showContextMenu({
mouseEvent: new MouseEvent("mousedown", { clientX, clientY }),
rowNode,
column,
value,
source: "api"
});
}
function showColumnChooser(beans, params) { function showColumnChooser(beans, params) {
beans.colChooserFactory?.showColumnChooser({ chooserParams: params }); beans.colChooserFactory?.showColumnChooser({ chooserParams: params });
} }
@@ -64155,6 +64534,15 @@ var ColumnMenuModule = {
}, },
dependsOn: [MenuCoreModule, SharedDragAndDropModule, ColumnMoveModule] dependsOn: [MenuCoreModule, SharedDragAndDropModule, ColumnMoveModule]
}; };
var ContextMenuModule = {
moduleName: "ContextMenu",
version: VERSION2,
beans: [ContextMenuService],
apiFunctions: {
showContextMenu
},
dependsOn: [MenuCoreModule]
};
var SET_FILTER_SELECT_ALL = "__AG_SELECT_ALL__"; var SET_FILTER_SELECT_ALL = "__AG_SELECT_ALL__";
var SET_FILTER_ADD_SELECTION_TO_FILTER = "__AG_ADD_SELECTION_TO_FILTER__"; var SET_FILTER_ADD_SELECTION_TO_FILTER = "__AG_ADD_SELECTION_TO_FILTER__";
var FlatSetDisplayValueModel = class { var FlatSetDisplayValueModel = class {
@@ -77586,7 +77974,8 @@ ModuleRegistry.registerModules([
NumberFilterModule, NumberFilterModule,
TextFilterModule, TextFilterModule,
SetFilterModule, SetFilterModule,
DateFilterModule DateFilterModule,
ContextMenuModule
]); ]);
var Grid = (props) => { var Grid = (props) => {
const { data, options, api, on, class: className, style = "height: 100%; width: 100%", dark } = props; const { data, options, api, on, class: className, style = "height: 100%; width: 100%", dark } = props;

File diff suppressed because one or more lines are too long

View File

@@ -27,6 +27,7 @@ import {
StatusBarModule, StatusBarModule,
ExcelExportModule, ExcelExportModule,
ClipboardModule, ClipboardModule,
ContextMenuModule
} from "../ag-grid"; } from "../ag-grid";
ModuleRegistry.registerModules([ ModuleRegistry.registerModules([
@@ -50,7 +51,8 @@ ModuleRegistry.registerModules([
NumberFilterModule, NumberFilterModule,
TextFilterModule, TextFilterModule,
SetFilterModule, SetFilterModule,
DateFilterModule DateFilterModule,
ContextMenuModule
]); ]);
const Grid = (props) => { const Grid = (props) => {