frontend/libs/canvasSpreadsheet/src/lib/components/Cells/iconUtils.ts (245 lines of code) (raw):

import * as PIXI from 'pixi.js'; import { AppTheme, ColumnDataType, GridCell } from '@frontend/common'; import { TotalType } from '@frontend/parser'; import { ComponentLayer, GridSizes } from '../../constants'; import { Cell, GridApi, GridCallbacks } from '../../types'; // TODO: Entire logic about icons option, path, tooltip, callbacks should be moved to the application level (mb to the GridCell) export function setCellIcon( cellData: GridCell, cell: Cell, gridCallbacks: GridCallbacks, gridApi: GridApi, themeName: AppTheme, gridSizes: GridSizes ): PIXI.Sprite | undefined { const iconOptions = getIconOptions(cellData, themeName); if (!iconOptions) return; const { path, tooltip } = iconOptions; const icon = PIXI.Sprite.from(path); const isApplyIcon = isApplyFieldHeaderCell(cellData); const isTotalIcon = cellData.totalIndex && cellData.totalType; const { fontSize, applyIconSize, totalIconSize } = gridSizes.cell; let iconSize = fontSize; if (isApplyIcon) { iconSize = applyIconSize; } else if (isTotalIcon) { iconSize = totalIconSize; } icon.zIndex = ComponentLayer.Icon; icon.roundPixels = true; icon.height = iconSize; icon.width = iconSize; icon.eventMode = 'static'; icon.addEventListener('pointerover', (e: PIXI.FederatedPointerEvent) => { icon.cursor = 'pointer'; const { x, y } = e.target as PIXI.Sprite; const tooltipX = x + icon.width / 2; const tooltipY = y + icon.height / 2; tooltip && gridApi.openTooltip(tooltipX, tooltipY, tooltip); }); icon.addEventListener('pointerout', () => { tooltip && gridApi.closeTooltip(); }); const shouldAddClickEvent = isIconClickable(cellData); if (!shouldAddClickEvent) return icon; const { col, row } = cellData; if (cellData.isTableHeader) { icon.addEventListener('pointerdown', () => gridCallbacks.onDeleteTable?.(cellData.table?.tableName || '') ); } else if (isApplyIcon) { icon.addEventListener('pointerdown', (e) => gridApi.openContextMenuAtCoords(e.screen.x, e.screen.y, col, row) ); } else if (cellData.field?.isNested || cellData.field?.isPeriodSeries) { icon.addEventListener('pointerdown', () => gridCallbacks.onExpandDimTable?.( cellData.table?.tableName || '', cellData.field?.fieldName || '', col, row ) ); } else if ( (cellData.field?.type === ColumnDataType.TABLE || cellData.field?.type === ColumnDataType.INPUT) && cellData.field?.referenceTableName ) { icon.addEventListener('pointerdown', () => gridCallbacks.onShowRowReference?.( cellData.table?.tableName || '', cellData.field?.fieldName || '', col, row ) ); } return icon; } export function setTableHeaderContextMenuIcon( cell: Cell, gridApi: GridApi, themeName: AppTheme, gridSizes: GridSizes ): PIXI.Sprite | undefined { const icon = PIXI.Sprite.from(getFullIconName('contextMenu', themeName)); const { fontSize } = gridSizes.cell; const { col, row } = cell; icon.zIndex = ComponentLayer.Icon; icon.roundPixels = true; icon.height = fontSize; icon.width = fontSize; icon.eventMode = 'static'; icon.addEventListener('pointerover', (e: PIXI.FederatedPointerEvent) => { icon.cursor = 'pointer'; const { x, y } = e.target as PIXI.Sprite; const tooltipX = x + icon.width / 2; const tooltipY = y + icon.height / 2; gridApi.openTooltip(tooltipX, tooltipY, 'Context Menu'); }); icon.addEventListener('pointerout', () => { gridApi.closeTooltip(); }); icon.addEventListener('pointerdown', (e) => gridApi.openContextMenuAtCoords(e.screen.x, e.screen.y, col, row) ); return icon; } export function getIconOptions( cell: GridCell, themeName: AppTheme ): { path: string; tooltip: string } | null { const isHeader = !!cell.isTableHeader; const isField = !!cell.isFieldHeader; const isCell = !isHeader && !isField; const isNestedIcon = isCell && cell.field?.isNested; const isPeriodSeriesIcon = cell.field?.isPeriodSeries && !isField; const isReferenceIcon = (cell.field?.type === ColumnDataType.TABLE || cell.field?.type === ColumnDataType.INPUT) && cell.field?.referenceTableName; if (isHeader) { return { path: getFullIconName('delete', themeName), tooltip: 'Delete table', }; } if (isApplyFieldHeaderCell(cell)) { return { path: getApplyIconPath(cell, themeName), tooltip: 'Sort/Filter' }; } if (cell.totalIndex) { if (!cell.totalType) return null; const icon = getTotalIcon(cell.totalType); if (icon) { return { path: getFullIconName(icon, themeName), tooltip: getTotalIconTooltip(cell.totalType), }; } } if (isNestedIcon) { const tooltip = cell.field?.referenceTableName ? 'Nested Table: ' + cell.field?.referenceTableName : ''; return { path: getFullIconName('table', themeName), tooltip }; } else if (isPeriodSeriesIcon) { return { path: getFullIconName('chart', themeName), tooltip: 'Period series', }; } else if (isReferenceIcon) { const tooltip = cell.field?.referenceTableName ? 'Reference: ' + cell.field?.referenceTableName : ''; return { path: getFullIconName('reference', themeName), tooltip }; } return null; } export function getFullIconName(iconName: string, themeName: AppTheme): string { const theme = themeName === AppTheme.ThemeDark ? 'Dark' : 'Light'; return `assets/icons/canvasGrid/${iconName}${theme}.svg`; } function getTotalIcon(totalType: TotalType): string | undefined { const mapping: [TotalType, string][] = [ ['sum', 'totalSum'], ['average', 'totalAverage'], ['count', 'totalCount'], ['stdevs', 'totalStdevs'], ['median', 'totalMedian'], ['mode', 'totalMode'], ['max', 'totalMax'], ['min', 'totalMin'], ['custom', 'totalCustom'], ]; for (const [type, icon] of mapping) { if (type === totalType) { return icon; } } return; } function getApplyIconPath(cell: GridCell, themeName: AppTheme): string { const sort = cell.field?.sort; let icon = 'arrowDown'; if (cell.field?.isFiltered) { if (sort === 'asc') { icon = 'filterSortAsc'; } else if (sort === 'desc') { icon = 'filterSortDesc'; } else { icon = 'filter'; } } else { if (sort === 'asc') { icon = 'sortAsc'; } else if (sort === 'desc') { icon = 'sortDesc'; } } return getFullIconName(icon, themeName); } export function getTotalIconTooltip(totalType: TotalType): string { const mapping: [TotalType, string][] = [ ['sum', 'Sum'], ['average', 'Average'], ['count', 'Count'], ['stdevs', 'Standard Deviation'], ['median', 'Median'], ['mode', 'Mode'], ['max', 'Max'], ['min', 'Min'], ['custom', 'Custom'], ]; for (const [type, tooltip] of mapping) { if (type === totalType) { return tooltip + ' total'; } } return 'Total'; } export function isFieldSortedOrFiltered(cell: GridCell): boolean { return !!cell.field?.sort || !!cell.field?.isFiltered; } export function isIconRightPlacement(cell?: GridCell): boolean { if (!cell) return false; const isTableHeader = !!cell.isTableHeader; return isApplyFieldHeaderCell(cell) || isTableHeader; } function isApplyFieldHeaderCell(cell: GridCell): boolean { return !!cell.isFieldHeader && !cell.field?.isDynamic; } function isTableReference(cell: GridCell): boolean { return !!( (cell.field?.type === ColumnDataType.TABLE || cell.field?.type === ColumnDataType.INPUT) && cell.field?.referenceTableName ); } function isIconClickable(cell: GridCell): boolean { return ( !!cell.isTableHeader || !!cell.field?.isNested || !!cell.field?.isPeriodSeries || isApplyFieldHeaderCell(cell) || isTableReference(cell) ); }