in frontend/libs/spreadsheet/src/lib/components/contextMenu/ContextMenu.tsx [65:318]
export function ContextMenu({
handleContextMenu,
gridServiceRef,
gridCallbacksRef,
api,
functions,
parsedSheets,
inputFiles,
}: Props) {
const [contextMenuOpen, setContextMenuOpen] = useState(false);
const [contextMenuPos, setContextMenuPos] = useState({ x: 0, y: 0 });
const [contextMenuItems, setContextMenuItems] = useState<MenuItem[]>([]);
const clickRef = useRef<HTMLDivElement>(null);
const { onClickContextMenu } = useOnClickContextMenu({
gridCallbacksRef,
gridServiceRef,
api,
});
const onClickOutside = useCallback(() => {
setContextMenuOpen(false);
}, []);
const handleCreateTableBySize = useCallback(
(cols: number, rows: number) => {
const colsItems = new Array(cols).fill('');
const rowsItems = new Array(rows).fill(colsItems);
gridCallbacksRef.current.onCreateManualTable?.(
api?.selection?.startCol ?? 1,
api?.selection?.startRow ?? 1,
rowsItems
);
},
[api, gridCallbacksRef]
);
useClickOutside(clickRef, onClickOutside);
const openContextMenu = useCallback(
(params: OpenContextMenuParams | null) => {
if (!params || !api) return;
const { x, y, col, row } = params;
const cell = gridServiceRef.current?.getCellValue(+row, +col);
setTimeout(() => {
setContextMenuOpen(true);
setContextMenuPos({ x, y });
if (!cell?.table) {
const contextCell = getCellContext(api, col, row);
if (!contextCell?.table) {
setContextMenuItems(
getEmptyCellWithoutContextMenuItem(
functions,
parsedSheets,
inputFiles,
handleCreateTableBySize,
col,
row
)
);
} else {
setContextMenuItems(getEmptyCellMenuItems(col, row, contextCell));
}
return;
}
if (cell.isTableHeader) {
setContextMenuItems(getTableHeaderMenuItems(cell));
} else if (cell.isFieldHeader && cell.field) {
setContextMenuItems(
getTableFieldMenuItems(col, row, cell, gridCallbacksRef.current)
);
} else {
setContextMenuItems(
getTableCellMenuItems(col, row, cell, gridCallbacksRef.current)
);
}
});
},
[
api,
gridServiceRef,
functions,
parsedSheets,
inputFiles,
handleCreateTableBySize,
gridCallbacksRef,
]
);
const onContextMenu = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
if (!api) return;
const { top, left } = e.currentTarget.getBoundingClientRect();
const { col, row } = getCellElementDimensions(e.target as HTMLElement);
if (col === -1 || row === -1) return;
openContextMenu({
x: e.clientX - left,
y: e.clientY - top,
col: +col,
row: +row,
});
},
[api, openContextMenu]
);
const onClick = useCallback(
(info: MenuInfo) => {
const { action } = JSON.parse(info.key);
if (action.startsWith('NumFilter') || action.startsWith('TextFilter')) {
return;
}
setContextMenuOpen(false);
// Timeout to hide context menu and focus spreadsheet first
// and avoid blur event to close just opened cell editor
setTimeout(() => {
onClickContextMenu(info);
}, 0);
},
[onClickContextMenu]
);
const onKeyUp = useCallback(
(event: KeyboardEvent) => {
if (contextMenuOpen && event.key === KeyboardCode.Escape) {
setContextMenuOpen(false);
return;
}
if (contextMenuOpen) return;
if (!shortcutApi.is(Shortcut.ContextMenu, event)) {
setContextMenuOpen(false);
return;
}
event.preventDefault();
if (!api) return;
const gridDataScroller = getDataScroller();
const root = getGridRoot();
const selection = api.selection$.getValue();
if (!selection || !gridDataScroller || !root) return;
const { startRow, startCol, endCol } = selection;
let cell = null;
for (let col = startCol; col <= endCol; col++) {
cell = gridDataScroller.querySelector(
`[data-row="${startRow}"][data-col="${col}"]`
);
if (cell) {
break;
}
}
if (!cell) return;
const { x, y, height } = cell.getBoundingClientRect();
const { top, left } = root.getBoundingClientRect();
openContextMenu({
x:
x - left >= 0
? x + defaults.cell.width / 2 - left
: defaults.cell.width,
y: y + height / 2 - top,
col: startCol,
row: startRow,
});
},
[api, contextMenuOpen, openContextMenu]
);
const onMouseDown = useCallback(
(event: any) => {
const target = findTargetButtonByClass(event, [
contextMenuButtonClass,
applyMenuButtonClass,
]);
if (!target) return;
openContextMenu(onContextMenuButton(event, target));
},
[openContextMenu]
);
useEffect(() => {
document.addEventListener('keyup', onKeyUp);
const dataContainer = document.querySelector(`.${gridDataContainerClass}`);
dataContainer?.addEventListener('click', onMouseDown);
return () => {
document.removeEventListener('keyup', onKeyUp);
dataContainer?.removeEventListener('click', onMouseDown);
};
}, [onMouseDown, onKeyUp]);
useEffect(() => {
handleContextMenu.current = onContextMenu;
}, [handleContextMenu, onContextMenu]);
useEffect(() => {
if (!contextMenuOpen) {
// Prevent unpredictable behavior, we should return focus to the spreadsheet
focusSpreadsheet();
}
}, [contextMenuOpen]);
return (
<Dropdown
autoAdjustOverflow={true}
autoFocus={true}
destroyPopupOnHide={true}
forceRender={true}
menu={{ items: contextMenuItems, onClick }}
open={contextMenuOpen}
rootClassName="grid-context-menu"
>
<div
id="area"
ref={clickRef}
style={{
position: 'absolute',
top: `${contextMenuPos.y}px`,
left: `${contextMenuPos.x}px`,
}}
/>
</Dropdown>
);
}