diff --git a/docs/assets/demo/en/custom-render/complex-corner.md b/docs/assets/demo/en/custom-render/complex-corner.md index e3fd9a5d76..18fcf213f2 100644 --- a/docs/assets/demo/en/custom-render/complex-corner.md +++ b/docs/assets/demo/en/custom-render/complex-corner.md @@ -71,9 +71,18 @@ const option = { }, corner: { titleOnDimension: 'row', + disableHeaderHover: true, + disableHeaderSelect: true, customLayout: args => { const { table, row, col, rect } = args; const { height, width } = rect ?? table.getCellRect(col, row); + const provinceWidth = table.getColWidth(0); + const cityWidth = table.getColWidth(1); + const categoryHeight = table.getRowHeight(0); + const provinceCenterX = provinceWidth / 2; + const cityCenterX = provinceWidth + cityWidth / 2; + const indicatorX = provinceWidth + cityWidth + 6; + const endCategoryX = width - 80; const container = createGroup({ height, width @@ -81,12 +90,12 @@ const option = { // 定义文本内容的数组 const texts = [ - { text: 'province', fontSize: 18, x: 10, y: rect.height - 25 }, - { text: 'city', fontSize: 18, x: 105, y: rect.height - 25 }, - { text: 'data', fontSize: 18, x: rect.width - 50, y: rect.height - 35 }, - { text: 'subCategory', fontSize: 18, x: rect.width - 100, y: rect.height - 85 }, - { text: 'category', fontSize: 18, x: rect.width - 80, y: 18 }, - { text: 'indicator', fontSize: 16, x: 176, y: rect.height - 20 } + { text: 'province', fontSize: 18, x: provinceCenterX - 36, y: height - 25 }, + { text: 'city', fontSize: 18, x: cityCenterX - 14, y: height - 25 }, + { text: 'data', fontSize: 18, x: endCategoryX + 30, y: height - 35 }, + { text: 'subCategory', fontSize: 18, x: endCategoryX - 20, y: height - 85 }, + { text: 'category', fontSize: 18, x: endCategoryX, y: Math.max(18, categoryHeight / 2 - 9) }, + { text: 'indicator', fontSize: 16, x: indicatorX, y: height - 20 } ]; // 循环添加文本 @@ -105,17 +114,16 @@ const option = { // 定义线段的点 const linePoints = [ - { x: rect.left, y: rect.top }, { x: 0, y: 0 }, - { x: rect.width - 40, y: rect.height }, + { x: width - 40, y: height }, { x: 0, y: 0 }, - { x: 173, y: rect.height }, + { x: provinceWidth + cityWidth + 3, y: height }, { x: 0, y: 0 }, - { x: 84, y: rect.height }, + { x: provinceWidth, y: height }, { x: 0, y: 0 }, - { x: rect.width, y: rect.height - 90 }, + { x: width, y: categoryHeight }, { x: 0, y: 0 }, - { x: rect.width, y: rect.height - 38 }, + { x: width, y: height - 38 }, { x: 0, y: 0 } ]; @@ -398,6 +406,10 @@ const option = { rightFrozenColCount: 1, dragOrder:{ dragHeaderMode: 'all' + }, + resize: { + columnResizeMode: 'all', + rowResizeMode: 'all' } }; diff --git a/docs/assets/demo/zh/custom-render/complex-corner.md b/docs/assets/demo/zh/custom-render/complex-corner.md index 7c60a70928..c14b8794f7 100644 --- a/docs/assets/demo/zh/custom-render/complex-corner.md +++ b/docs/assets/demo/zh/custom-render/complex-corner.md @@ -71,9 +71,18 @@ const option = { }, corner: { titleOnDimension: 'row', + disableHeaderHover: true, + disableHeaderSelect: true, customLayout: args => { const { table, row, col, rect } = args; const { height, width } = rect ?? table.getCellRect(col, row); + const provinceWidth = table.getColWidth(0); + const cityWidth = table.getColWidth(1); + const categoryHeight = table.getRowHeight(0); + const provinceCenterX = provinceWidth / 2; + const cityCenterX = provinceWidth + cityWidth / 2; + const indicatorX = provinceWidth + cityWidth + 6; + const endCategoryX = width - 50; const container = createGroup({ height, width @@ -81,12 +90,12 @@ const option = { // 定义文本内容的数组 const texts = [ - { text: '省份', fontSize: 18, x: 30, y: rect.height - 25 }, - { text: '城市', fontSize: 18, x: 105, y: rect.height - 25 }, - { text: '数据', fontSize: 18, x: rect.width - 50, y: rect.height - 35 }, - { text: '子类别', fontSize: 18, x: rect.width - 50, y: rect.height - 85 }, - { text: '类别', fontSize: 18, x: rect.width - 50, y: 18 }, - { text: '指标', fontSize: 16, x: 176, y: rect.height - 20 } + { text: '省份', fontSize: 18, x: provinceCenterX - 18, y: height - 25 }, + { text: '城市', fontSize: 18, x: cityCenterX - 18, y: height - 25 }, + { text: '数据', fontSize: 18, x: endCategoryX, y: height - 35 }, + { text: '子类别', fontSize: 18, x: endCategoryX, y: height - 85 }, + { text: '类别', fontSize: 18, x: endCategoryX, y: Math.max(18, categoryHeight / 2 - 9) }, + { text: '指标', fontSize: 16, x: indicatorX, y: height - 20 } ]; // 循环添加文本 @@ -105,17 +114,16 @@ const option = { // 定义线段的点 const linePoints = [ - { x: rect.left, y: rect.top }, { x: 0, y: 0 }, - { x: rect.width - 40, y: rect.height }, + { x: width - 40, y: height }, { x: 0, y: 0 }, - { x: 173, y: rect.height }, + { x: provinceWidth + cityWidth + 3, y: height }, { x: 0, y: 0 }, - { x: 84, y: rect.height }, + { x: provinceWidth, y: height }, { x: 0, y: 0 }, - { x: rect.width, y: rect.height - 90 }, + { x: width, y: categoryHeight }, { x: 0, y: 0 }, - { x: rect.width, y: rect.height - 38 }, + { x: width, y: height - 38 }, { x: 0, y: 0 } ]; @@ -398,6 +406,10 @@ const option = { rightFrozenColCount: 1, dragOrder: { dragHeaderMode: 'all' + }, + resize: { + columnResizeMode: 'all', + rowResizeMode: 'all' } }; diff --git a/packages/react-vtable/src/table-components/custom/custom-layout.tsx b/packages/react-vtable/src/table-components/custom/custom-layout.tsx index 9731ded216..7f3f904a47 100644 --- a/packages/react-vtable/src/table-components/custom/custom-layout.tsx +++ b/packages/react-vtable/src/table-components/custom/custom-layout.tsx @@ -106,10 +106,11 @@ export const CustomLayout: React.FC = (props: PropsWithChildr const key = `${col}-${row}`; if (container.current.has(key)) { const currentContainer = container.current.get(key); - module.reconcilor.updateContainer(null, currentContainer, null); - // group = currentContainer.containerInfo; - currentContainer.containerInfo.delete(); - container.current.delete(key); + reconcilorUnmountContainer(module, currentContainer); + if (container.current.get(key) === currentContainer) { + currentContainer.containerInfo.delete(); + container.current.delete(key); + } } }, []); @@ -119,12 +120,20 @@ export const CustomLayout: React.FC = (props: PropsWithChildr container.current.clear(); return; } - container.current.forEach((value, key) => { - const currentContainer = value; - module.reconcilor.updateContainer(null, currentContainer, null); - currentContainer.containerInfo.delete(); + const pendingContainers = Array.from(container.current.entries()); + batchReconcilerUpdates(module, () => { + pendingContainers.forEach(([, currentContainer]) => { + requestReconcilerUnmountContainer(module, currentContainer); + }); + }); + flushReconcilerWork(module); + + pendingContainers.forEach(([key, currentContainer]) => { + if (container.current.get(key) === currentContainer) { + currentContainer.containerInfo.delete(); + container.current.delete(key); + } }); - container.current.clear(); }, []); useLayoutEffect(() => { @@ -219,10 +228,7 @@ function reconcilorUpdateContainer(module: ReconcilerModule, children: ReactElem const updateContainerSync = (reconcilor as any).updateContainerSync; if (typeof updateContainerSync === 'function') { updateContainerSync(element, currentContainer, null); - const flushSyncWork = (reconcilor as any).flushSyncWork; - if (typeof flushSyncWork === 'function') { - flushSyncWork(); - } + flushReconcilerWork(module); return; } reconcilor.updateContainer(element, currentContainer, null); @@ -237,6 +243,60 @@ function reconcilorUpdateContainer(module: ReconcilerModule, children: ReactElem // } } +function reconcilorUnmountContainer(module: ReconcilerModule, currentContainer: any): boolean { + requestReconcilerUnmountContainer(module, currentContainer); + return flushReconcilerWork(module); +} + +function requestReconcilerUnmountContainer(module: ReconcilerModule, currentContainer: any) { + const { reconcilor } = module; + const updateContainerSync = (reconcilor as any).updateContainerSync; + if (typeof updateContainerSync === 'function') { + try { + updateContainerSync(null, currentContainer, null); + return; + } catch { + reconcilor.updateContainer(null, currentContainer, null); + return; + } + } + reconcilor.updateContainer(null, currentContainer, null); +} + +function flushReconcilerWork(module: ReconcilerModule): boolean { + const { reconcilor } = module; + const flushSyncWork = (reconcilor as any).flushSyncWork; + if (typeof flushSyncWork === 'function') { + try { + const result = flushSyncWork(); + if (result === true) { + return false; + } + } catch { + return false; + } + } + + const flushPassiveEffects = (reconcilor as any).flushPassiveEffects; + if (typeof flushPassiveEffects === 'function') { + try { + flushPassiveEffects(); + } catch { + return false; + } + } + return true; +} + +function batchReconcilerUpdates(module: ReconcilerModule, callback: () => void) { + const batchedUpdates = (module.reconcilor as any).batchedUpdates; + if (typeof batchedUpdates === 'function') { + batchedUpdates(callback); + } else { + callback(); + } +} + function getCellRect(col: number, row: number, table: any) { const range = table.getCellRange(col, row); const rect = table.getCellsRect(range.start.col, range.start.row, range.end.col, range.end.row); diff --git a/packages/vtable/examples/custom/complex-corner.ts b/packages/vtable/examples/custom/complex-corner.ts index 5948f97184..6701fecb0a 100644 --- a/packages/vtable/examples/custom/complex-corner.ts +++ b/packages/vtable/examples/custom/complex-corner.ts @@ -1,397 +1,406 @@ import * as VTable from '../../src'; -import { bindDebugTool } from '../../src/scenegraph/debug-tool'; +import VChart from '@visactor/vchart'; const BUGSERVER_CONTAINER_ID = 'vTable'; -const VCHART_NPM_CDN = 'https://unpkg.com/@visactor/vchart@latest/build/index.min.js'; +VTable.register.chartModule('vchart', VChart); -// your code here -import('https://unpkg.com/@visactor/vchart@latest/build/index.min.js') - .then(async () => { - VTable.register.chartModule('vchart', window.VChart.default); - const dom = document.querySelector('#' + BUGSERVER_CONTAINER_ID); - dom.style.width = '800px'; - dom.style.height = '800px'; - const option = { - autoFillHeight: true, - rows: ['province', 'city'], - columns: ['category', 'sub_category'], - indicators: ['sales', 'number'], - defaultHeaderColWidth: ['auto', 'auto', 120], - defaultHeaderRowHeight: ['auto', 90], - indicatorTitle: '指标名称', - indicatorsAsCol: false, - customMergeCell: (col, row, table) => { - if (col >= 0 && col < 3 && row <= 1) { - return { - text: 'merge text', - range: { - start: { - col: 0, - row: 0 - }, - end: { - col: 2, - row: 1 - } +export function createTable() { + const dom = document.querySelector('#' + BUGSERVER_CONTAINER_ID); + dom.style.width = '800px'; + dom.style.height = '800px'; + const option = { + autoFillHeight: true, + rows: ['province', 'city'], + columns: ['category', 'sub_category'], + indicators: ['sales', 'number'], + defaultHeaderColWidth: ['auto', 'auto', 120], + defaultHeaderRowHeight: ['auto', 90], + indicatorTitle: '指标名称', + indicatorsAsCol: false, + customMergeCell: (col, row, table) => { + if (col >= 0 && col < 3 && row <= 1) { + return { + text: 'merge text', + range: { + start: { + col: 0, + row: 0 }, - style: { - bgColor: '#ECF1F5' + end: { + col: 2, + row: 1 } - }; - } - }, - dataConfig: { - totals: { - row: { - showGrandTotals: true, - showSubTotals: true, - subTotalsDimensions: ['province'], - grandTotalLabel: '行总计', - subTotalLabel: '小计' }, - column: { - showGrandTotals: true, - showSubTotals: true, - subTotalsDimensions: ['category'], - grandTotalLabel: '列总计', - subTotalLabel: '小计' + style: { + bgColor: '#ECF1F5' } + }; + } + }, + dataConfig: { + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['province'], + grandTotalLabel: '行总计', + subTotalLabel: '小计' + }, + column: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['category'], + grandTotalLabel: '列总计', + subTotalLabel: '小计' } - }, - corner: { - merge: true, - titleOnDimension: 'row', - customLayout: (args: any) => { - const {table, row, col, rect} = args; - const {height, width} = rect ?? table.getCellRect(col, row); - const container = new VTable.CustomLayout.Group({ - height, - width, - }); - - // 定义文本内容的数组 - const texts = [ - {text: '省', fontSize: 18, x: 30, y: rect.height - 25}, - {text: '市', fontSize: 18, x: 105, y: rect.height - 25}, - {text: '数据', fontSize: 18, x: rect.width - 50, y: rect.height - 35}, - {text: '物品', fontSize: 18, x: rect.width - 50, y: rect.height - 85}, - {text: '种类', fontSize: 18, x: rect.width - 50, y: 18}, - {text: '类型', fontSize: 16, x: 176, y: rect.height - 20}, - ]; + } + }, + corner: { + merge: true, + titleOnDimension: 'row', + disableHeaderHover: true, + disableHeaderSelect: true, + customLayout: (args: any) => { + const { table, row, col, rect } = args; + const { height, width } = rect ?? table.getCellRect(col, row); + const provinceWidth = table.getColWidth(0); + const cityWidth = table.getColWidth(1); + const categoryHeight = table.getRowHeight(0); + const provinceCenterX = provinceWidth / 2; + const cityCenterX = provinceWidth + cityWidth / 2; + const indicatorX = provinceWidth + cityWidth + 6; + const endCategoryX = width - 50; + const container = new VTable.CustomLayout.Group({ + height, + width + }); - // 循环添加文本 - texts.forEach(({text, fontSize, x, y}) => { - container.addChild( - new VTable.CustomLayout.Text({ - text, - fontSize, - fontFamily: 'sans-serif', - fill: 'black', - x, - y, - }) - ); - }); + // 定义文本内容的数组 + const texts = [ + { text: '省份', fontSize: 18, x: provinceCenterX - 18, y: height - 25 }, + { text: '城市', fontSize: 18, x: cityCenterX - 18, y: height - 25 }, + { text: '数据', fontSize: 18, x: endCategoryX, y: height - 35 }, + { text: '子类别', fontSize: 18, x: endCategoryX, y: height - 85 }, + { text: '类别', fontSize: 18, x: endCategoryX, y: Math.max(18, categoryHeight / 2 - 9) }, + { text: '指标', fontSize: 16, x: indicatorX, y: height - 20 } + ]; - // 定义线段的点 - const linePoints = [ - {x: rect.left, y: rect.top}, - {x: rect.width - 40, y: rect.height}, - {x: 173, y: rect.height}, - {x: 0, y: 0}, - {x: 84, y: rect.height}, - {x: 0, y: 0}, - {x: rect.width, y: rect.height - 90}, - {x: 0, y: 0}, - {x: rect.width, y: rect.height - 38}, - {x: 0, y: 0}, - ]; - - // 添加线段 + // 循环添加文本 + texts.forEach(({ text, fontSize, x, y }) => { container.addChild( - new VTable.CustomLayout.Line({ - points: linePoints, - lineWidth: 1, - stroke: '#ccc', + new VTable.CustomLayout.Text({ + text, + fontSize, + fontFamily: 'sans-serif', + fill: 'black', + x, + y }) ); + }); - return { - rootContainer: container, - renderDefault: false, - enableCellPadding: false, - }; - } + // 定义线段的点 + const linePoints = [ + { x: 0, y: 0 }, + { x: width - 40, y: height }, + { x: 0, y: 0 }, + { x: provinceWidth + cityWidth + 3, y: height }, + { x: 0, y: 0 }, + { x: provinceWidth, y: height }, + { x: 0, y: 0 }, + { x: width, y: categoryHeight }, + { x: 0, y: 0 }, + { x: width, y: height - 38 }, + { x: 0, y: 0 } + ]; + + // 添加线段 + container.addChild( + new VTable.CustomLayout.Line({ + points: linePoints, + lineWidth: 1, + stroke: '#ccc' + }) + ); + + return { + rootContainer: container, + renderDefault: false, + enableCellPadding: false + }; + } + }, + records: [ + { + sales: 891, + number: 77899999, + province: '浙江省', + city: '杭州市', + category: '家具', + sub_category: '桌子' }, - records: [ - { - sales: 891, - number: 77899999, - province: '浙江省', - city: '杭州市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 792, - number: 2367, - province: '浙江省', - city: '绍兴市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 893, - number: 3877, - province: '浙江省', - city: '宁波市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 1094, - number: 4342, - province: '浙江省', - city: '舟山市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 1295, - number: 5343, - province: '浙江省', - city: '杭州市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 496, - number: 632, - province: '浙江省', - city: '绍兴市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 1097, - number: 7234, - province: '浙江省', - city: '宁波市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 998, - number: 834, - province: '浙江省', - city: '舟山市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 766, - number: 945, - province: '浙江省', - city: '杭州市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 990, - number: 1304, - province: '浙江省', - city: '绍兴市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 891, - number: 1145, - province: '浙江省', - city: '宁波市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 792, - number: 1432, - province: '浙江省', - city: '舟山市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 745, - number: 1343, - province: '浙江省', - city: '杭州市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 843, - number: 1354, - province: '浙江省', - city: '绍兴市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 895, - number: 1523, - province: '浙江省', - city: '宁波市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 965, - number: 1634, - province: '浙江省', - city: '舟山市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 776, - number: 1723, - province: '四川省', - city: '成都市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 634, - number: 1822, - province: '四川省', - city: '绵阳市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 909, - number: 1943, - province: '四川省', - city: '南充市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 399, - number: 2330, - province: '四川省', - city: '乐山市', - category: '家具', - sub_category: '桌子' - }, - { - sales: 700, - number: 2451, - province: '四川省', - city: '成都市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 689, - number: 2244, - province: '四川省', - city: '绵阳市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 500, - number: 2333, - province: '四川省', - city: '南充市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 800, - number: 2445, - province: '四川省', - city: '乐山市', - category: '家具', - sub_category: '沙发' - }, - { - sales: 1044, - number: 2335, - province: '四川省', - city: '成都市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 689, - number: 245, - province: '四川省', - city: '绵阳市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 794, - number: 2457, - province: '四川省', - city: '南充市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 566, - number: 2458, - province: '四川省', - city: '乐山市', - category: '办公用品', - sub_category: '笔' - }, - { - sales: 865, - number: 4004, - province: '四川省', - city: '成都市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 999, - number: 3077, - province: '四川省', - city: '绵阳市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 999, - number: 3551, - province: '四川省', - city: '南充市', - category: '办公用品', - sub_category: '纸张' - }, - { - sales: 999, - number: 352, - province: '四川省', - city: '乐山市', - category: '办公用品', - sub_category: '纸张' - } - ], - widthMode: 'standard', // 宽度模式:standard 标准模式; adaptive 自动填满容器 - bottomFrozenRowCount: 2, - rightFrozenColCount: 1, + { + sales: 792, + number: 2367, + province: '浙江省', + city: '绍兴市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 893, + number: 3877, + province: '浙江省', + city: '宁波市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 1094, + number: 4342, + province: '浙江省', + city: '舟山市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 1295, + number: 5343, + province: '浙江省', + city: '杭州市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 496, + number: 632, + province: '浙江省', + city: '绍兴市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 1097, + number: 7234, + province: '浙江省', + city: '宁波市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 998, + number: 834, + province: '浙江省', + city: '舟山市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 766, + number: 945, + province: '浙江省', + city: '杭州市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 990, + number: 1304, + province: '浙江省', + city: '绍兴市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 891, + number: 1145, + province: '浙江省', + city: '宁波市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 792, + number: 1432, + province: '浙江省', + city: '舟山市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 745, + number: 1343, + province: '浙江省', + city: '杭州市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 843, + number: 1354, + province: '浙江省', + city: '绍兴市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 895, + number: 1523, + province: '浙江省', + city: '宁波市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 965, + number: 1634, + province: '浙江省', + city: '舟山市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 776, + number: 1723, + province: '四川省', + city: '成都市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 634, + number: 1822, + province: '四川省', + city: '绵阳市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 909, + number: 1943, + province: '四川省', + city: '南充市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 399, + number: 2330, + province: '四川省', + city: '乐山市', + category: '家具', + sub_category: '桌子' + }, + { + sales: 700, + number: 2451, + province: '四川省', + city: '成都市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 689, + number: 2244, + province: '四川省', + city: '绵阳市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 500, + number: 2333, + province: '四川省', + city: '南充市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 800, + number: 2445, + province: '四川省', + city: '乐山市', + category: '家具', + sub_category: '沙发' + }, + { + sales: 1044, + number: 2335, + province: '四川省', + city: '成都市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 689, + number: 245, + province: '四川省', + city: '绵阳市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 794, + number: 2457, + province: '四川省', + city: '南充市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 566, + number: 2458, + province: '四川省', + city: '乐山市', + category: '办公用品', + sub_category: '笔' + }, + { + sales: 865, + number: 4004, + province: '四川省', + city: '成都市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 999, + number: 3077, + province: '四川省', + city: '绵阳市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 999, + number: 3551, + province: '四川省', + city: '南充市', + category: '办公用品', + sub_category: '纸张' + }, + { + sales: 999, + number: 352, + province: '四川省', + city: '乐山市', + category: '办公用品', + sub_category: '纸张' + } + ], + widthMode: 'standard', // 宽度模式:standard 标准模式; adaptive 自动填满容器 + bottomFrozenRowCount: 2, + rightFrozenColCount: 1, + dragOrder: { dragHeaderMode: 'all' - }; + }, + resize: { + columnResizeMode: 'all', + rowResizeMode: 'all' + } + }; - option.container = dom; - const instance = new VTable.PivotTable(option); - bindDebugTool(instance.scenegraph.stage, { customGrapicKeys: ['col', 'row'] }); - window['tableInstance'] = instance; - }) - .catch(err => { - console.error('Failed import CDN resource: ', err); - }); + option.container = dom; + const instance = new VTable.PivotTable(option); + window.tableInstance = instance; +} diff --git a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts index bc7d557538..bdc4cf3e64 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts @@ -43,6 +43,11 @@ import { onBeforeAttributeUpdateForInvertHighlight } from '../../plugins/invert- import { getCellBorderStrokeWidth } from '../utils/cell-border-stroke-width'; import type { CreateSwitchCellGroup } from './cell-type/switch-cell'; import type { CreateButtonCellGroup } from './cell-type/button-cell'; +import { + createCornerCustomMergeContainer, + isCornerCustomMergeRange, + shouldRenderCornerCustomMergeContent +} from '../utils/corner-custom-merge'; const PROMISE_CELL_UPDATE_TOKEN_KEY = '__vtable_promise_cell_update_token__'; @@ -590,9 +595,23 @@ function _generateCustomElementsGroup( ) { let customElementsGroup; let renderDefault = true; + const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, range, table); + if (!shouldRenderContent) { + return { + customElementsGroup, + renderDefault: false + }; + } + if (customResult) { // custom merge custom render - customElementsGroup = customResult.elementsGroup; + customElementsGroup = createCornerCustomMergeContainer( + customResult.elementsGroup, + cellWidth, + cellHeight, + range, + table + ); renderDefault = customResult.renderDefault; } else if (range?.isCustom && !table.isCornerHeader(col, row)) { // 判断不是角头单元格,来兼容corner中设置的customLayout @@ -610,11 +629,13 @@ function _generateCustomElementsGroup( customLayout = define?.customLayout; } if (customLayout || customRender) { + const customCol = range && isCornerCustomMergeRange(range, table) ? range.start.col : col; + const customRow = range && isCornerCustomMergeRange(range, table) ? range.start.row : row; const customResult = dealWithCustom( customLayout, customRender, - col, - row, + customCol, + customRow, cellWidth, cellHeight, false, @@ -623,7 +644,13 @@ function _generateCustomElementsGroup( range, table ); - customElementsGroup = customResult.elementsGroup; + customElementsGroup = createCornerCustomMergeContainer( + customResult.elementsGroup, + cellWidth, + cellHeight, + range, + table + ); renderDefault = customResult.renderDefault; } } @@ -675,7 +702,7 @@ export function updateCell( // cellTheme.group.cornerRadius = getCellCornerRadius(col, row, table); // } - if (customLayout || customRender) { + if ((customLayout || customRender) && shouldRenderCornerCustomMergeContent(col, row, customMergeRange, table)) { customResult = dealWithCustom( customLayout, customRender, @@ -1364,6 +1391,24 @@ export function getCustomCellMergeCustom(col: number, row: number, cellGroup: Gr } = customMerge; if (customMergeLayout || customMergeRender) { + const rangeHeight = table.getRowHeight(row); + const rangeWidth = table.getColWidth(col); + const contentWidth = table.getColsWidth(customMergeRange.start.col, customMergeRange.end.col); + const contentHeight = table.getRowsHeight(customMergeRange.start.row, customMergeRange.end.row); + + cellGroup.contentWidth = contentWidth; + cellGroup.contentHeight = contentHeight; + + if (!shouldRenderCornerCustomMergeContent(col, row, customMergeRange, table)) { + const customContainer = cellGroup.getChildByName(CUSTOM_CONTAINER_NAME); + if (customContainer) { + cellGroup.removeChild(customContainer); + } + cellGroup.setAttribute('clip', true); + resizeCellGroup(cellGroup, rangeWidth, rangeHeight, customMergeRange, table); + return customMergeRange; + } + const customResult = dealWithCustom( customMergeLayout, customMergeRender, @@ -1378,7 +1423,13 @@ export function getCustomCellMergeCustom(col: number, row: number, cellGroup: Gr table ); - const customElementsGroup = customResult.elementsGroup; + const customElementsGroup = createCornerCustomMergeContainer( + customResult.elementsGroup, + contentWidth, + contentHeight, + customMergeRange, + table + ); if (cellGroup.childrenCount > 0 && customElementsGroup) { cellGroup.insertBefore(customElementsGroup, cellGroup.firstChild); @@ -1386,15 +1437,10 @@ export function getCustomCellMergeCustom(col: number, row: number, cellGroup: Gr cellGroup.appendChild(customElementsGroup); } - const rangeHeight = table.getRowHeight(row); - const rangeWidth = table.getColWidth(col); - - const { width: contentWidth } = cellGroup.attribute; - const { height: contentHeight } = cellGroup.attribute; - cellGroup.contentWidth = contentWidth; - cellGroup.contentHeight = contentHeight; - resizeCellGroup(cellGroup, rangeWidth, rangeHeight, customMergeRange, table); + if (isCornerCustomMergeRange(customMergeRange, table)) { + cellGroup.setAttribute('clip', false); + } return customMergeRange; } diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts index 8ce3149af2..36ed060fc2 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/text-cell.ts @@ -9,6 +9,7 @@ import type { BaseTableAPI } from '../../../ts-types/base-table'; import type { CellRange } from '../../../ts-types'; import { getCellBorderStrokeWidth } from '../../utils/cell-border-stroke-width'; import { createMark } from '../../graphic/mark'; +import { isCornerCustomMergeRange } from '../../utils/corner-custom-merge'; /** * @description: 创建单元格场景节点 @@ -42,12 +43,15 @@ export function createCellGroup( textAlign: CanvasTextAlign, textBaseline: CanvasTextBaseline, mayHaveIcon: boolean, - customElementsGroup: VGroup, + customElementsGroup: VGroup | undefined, renderDefault: boolean, cellTheme: IThemeSpec, range: CellRange | undefined, isAsync: boolean ): Group { + const isCornerCustomMergeContentCell = + isCornerCustomMergeRange(range, table) && col === range.end.col && row === range.end.row; + // The carrier cell must allow the clipped merge container to span internal cell bounds. const headerStyle = table._getCellStyle(col, row); // to be fixed const functionalPadding = getFunctionalProp('padding', headerStyle, col, row, table); if (isValid(functionalPadding)) { @@ -84,7 +88,7 @@ export function createCellGroup( cursor: (cellTheme?.group as any)?.cursor ?? undefined, lineDash: cellTheme?.group?.lineDash ?? undefined, lineCap: 'butt', - clip: true, + clip: !isCornerCustomMergeContentCell, cornerRadius: cellTheme.group.cornerRadius } as any); } @@ -106,7 +110,7 @@ export function createCellGroup( lineCap: 'butt', - clip: true, + clip: !isCornerCustomMergeContentCell, cornerRadius: cellTheme.group.cornerRadius } as any); @@ -118,6 +122,12 @@ export function createCellGroup( if (customElementsGroup) { cellGroup.appendChild(customElementsGroup); } + if (isCornerCustomMergeContentCell) { + cellGroup.setAttributes({ + width: Math.max(cellGroup.attribute.width, customElementsGroup?.attribute.width ?? 0), + height: Math.max(cellGroup.attribute.height, customElementsGroup?.attribute.height ?? 0) + }); + } if (renderDefault) { const textStr: string = value; let icons; diff --git a/packages/vtable/src/scenegraph/layout/update-height.ts b/packages/vtable/src/scenegraph/layout/update-height.ts index 55a70bc19b..517622040d 100644 --- a/packages/vtable/src/scenegraph/layout/update-height.ts +++ b/packages/vtable/src/scenegraph/layout/update-height.ts @@ -18,8 +18,25 @@ import type { IGraphic } from '@src/vrender'; import { getCellMergeRange } from '../../tools/merge-range'; import type { ColumnDefine, ListTableConstructorOptions } from '../../ts-types'; import { Factory } from '../../core/factory'; +import { + type CornerCustomMergeRangeUpdateMap, + createCornerCustomMergeContainer, + flushCornerCustomMergeContentUpdates, + isCornerCustomMergeRange, + queueCornerCustomMergeContentUpdate, + shouldRenderCornerCustomMergeContent +} from '../utils/corner-custom-merge'; + +export function updateRowHeight( + scene: Scenegraph, + row: number, + detaY: number, + skipTableHeightMap?: boolean, + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap +) { + const shouldFlushCornerCustomMergeRanges = !pendingCornerCustomMergeRanges; + pendingCornerCustomMergeRanges ??= new Map(); -export function updateRowHeight(scene: Scenegraph, row: number, detaY: number, skipTableHeightMap?: boolean) { // 更新table行高存储 if (!skipTableHeightMap && detaY) { scene.table._setRowHeight(row, scene.table.getRowHeight(row) + detaY, true); @@ -37,7 +54,16 @@ export function updateRowHeight(scene: Scenegraph, row: number, detaY: number, s const height = cell.attribute.height; // cell.setAttribute('height', height); // (cell.firstChild as Rect).setAttribute('height', cell.attribute.height); - updateCellHeightForRow(scene, cell, col, row, height + detaY, detaY, scene.table.isHeader(col, row)); + updateCellHeightForRow( + scene, + cell, + col, + row, + height + detaY, + detaY, + scene.table.isHeader(col, row), + pendingCornerCustomMergeRanges + ); scene.updateCellContentWhileResize(col, row); } @@ -70,6 +96,10 @@ export function updateRowHeight(scene: Scenegraph, row: number, detaY: number, s } } } + + if (shouldFlushCornerCustomMergeRanges) { + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, scene.table); + } } /** @@ -83,7 +113,8 @@ export function updateCellHeightForRow( row: number, height: number, detaY: number, - isHeader: boolean + isHeader: boolean, + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap // autoRowHeight: boolean ) { // cell.setAttribute('height', height); @@ -95,7 +126,7 @@ export function updateCellHeightForRow( return; } - updateCellHeight(scene, cellGroup, col, row, distHeight, detaY, isHeader); + updateCellHeight(scene, cellGroup, col, row, distHeight, detaY, isHeader, pendingCornerCustomMergeRanges); } export function updateCellHeightForColumn( @@ -119,7 +150,8 @@ export function updateCellHeight( row: number, distHeight: number, detaY: number, - isHeader: boolean + isHeader: boolean, + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap ) { if (cell.attribute.height === distHeight && !cell.needUpdateHeight) { return; @@ -168,7 +200,16 @@ export function updateCellHeight( oldBarCell.release(); // deal with text - updateMergeCellContentHeight(cell, distHeight, detaY, scene.table.isAutoRowHeight(row), true, scene.table); + updateMergeCellContentHeight( + cell, + distHeight, + detaY, + scene.table.isAutoRowHeight(row), + true, + scene.table, + true, + pendingCornerCustomMergeRanges + ); } else if (type === 'sparkline') { // 目前先采用重新生成节点的方案 cell.removeAllChild(); @@ -199,9 +240,15 @@ export function updateCellHeight( (cell.getChildByName(CUSTOM_CONTAINER_NAME) as Group) || (cell.getChildByName(CUSTOM_MERGE_CONTAINER_NAME) as Group); if (customContainer) { - // if (scene.table.reactCustomLayout) { - // scene.table.reactCustomLayout.removeCustomCell(col, row); - // } + if (scene.table.reactCustomLayout) { + const mergeRange = isMergeCellGroup(cell) + ? scene.table.getCellRange(cell.mergeStartCol, cell.mergeStartRow) + : undefined; + const shouldUseMergeStart = mergeRange && isCornerCustomMergeRange(mergeRange, scene.table); + const removeCol = shouldUseMergeStart ? mergeRange.start.col : col; + const removeRow = shouldUseMergeStart ? mergeRange.start.row : row; + scene.table.reactCustomLayout.removeCustomCell(removeCol, removeRow); + } // customContainer.removeAllChild(); let customElementsGroup; cell.removeChild(customContainer); @@ -216,8 +263,10 @@ export function updateCellHeight( const customContainer = (mergedCell.getChildByName(CUSTOM_CONTAINER_NAME) as Group) || (mergedCell.getChildByName(CUSTOM_MERGE_CONTAINER_NAME) as Group); - customContainer.removeAllChild(); - mergedCell.removeChild(customContainer); + if (customContainer) { + customContainer.removeAllChild(); + mergedCell.removeChild(customContainer); + } getCustomCellMergeCustom(col, mergeRow, mergedCell, scene.table); } } else { @@ -248,7 +297,13 @@ export function updateCellHeight( } } - if (customLayout || customRender) { + const mergeRange = isMergeCellGroup(cell) + ? scene.table.getCellRange(cell.mergeStartCol, cell.mergeStartRow) + : undefined; + const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table); + if (!shouldRenderContent) { + renderDefault = false; + } else if (customLayout || customRender) { // const { autoRowHeight } = table.internalProps; const style = scene.table._getCellStyle(col, row) as ProgressBarStyle; const padding = getQuadProps(getProp('padding', style, col, row, scene.table)); @@ -258,25 +313,32 @@ export function updateCellHeight( width = scene.table.getColsWidth(cell.mergeStartCol, cell.mergeEndCol); height = scene.table.getRowsHeight(cell.mergeStartRow, cell.mergeEndRow); } + let customCol = col; + let customRow = row; + if (mergeRange && isCornerCustomMergeRange(mergeRange, scene.table)) { + customCol = mergeRange.start.col; + customRow = mergeRange.start.row; + } const customResult = dealWithCustom( customLayout, customRender, - col, - row, + customCol, + customRow, width, height, false, scene.table.isAutoRowHeight(row), padding, - isMergeCellGroup(cell) - ? { - start: { col: cell.mergeStartCol, row: cell.mergeStartRow }, - end: { col: cell.mergeEndCol, row: cell.mergeEndRow } - } - : undefined, + mergeRange, + scene.table + ); + customElementsGroup = createCornerCustomMergeContainer( + customResult.elementsGroup, + width, + height, + mergeRange, scene.table ); - customElementsGroup = customResult.elementsGroup; renderDefault = customResult.renderDefault; } @@ -285,6 +347,12 @@ export function updateCellHeight( } else if (customElementsGroup) { cell.appendChild(customElementsGroup); } + if (isMergeCellGroup(cell)) { + const mergeRange = scene.table.getCellRange(cell.mergeStartCol, cell.mergeStartRow); + if (isCornerCustomMergeRange(mergeRange, scene.table)) { + cell.setAttribute('clip', !shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table)); + } + } } } // if (renderDefault) { @@ -301,7 +369,16 @@ export function updateCellHeight( // scene.table // ); // } - updateMergeCellContentHeight(cell, distHeight, detaY, scene.table.isAutoRowHeight(row), renderDefault, scene.table); + updateMergeCellContentHeight( + cell, + distHeight, + detaY, + scene.table.isAutoRowHeight(row), + renderDefault, + scene.table, + !customContainer, + pendingCornerCustomMergeRanges + ); } } @@ -311,7 +388,9 @@ function updateMergeCellContentHeight( detaY: number, autoRowHeight: boolean, renderDefault: boolean, - table: BaseTableAPI + table: BaseTableAPI, + refreshCornerCustomMergeContent = true, + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap ) { if (isMergeCellGroup(cellGroup)) { distHeight = 0; @@ -375,6 +454,10 @@ function updateMergeCellContentHeight( } } } + const mergeRange = table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); + if (refreshCornerCustomMergeContent && isCornerCustomMergeRange(mergeRange, table)) { + queueCornerCustomMergeContentUpdate(mergeRange, table, pendingCornerCustomMergeRanges); + } } else { const style = table._getCellStyle(cellGroup.col, cellGroup.row); const padding = getQuadProps(getProp('padding', style, cellGroup.col, cellGroup.row, table)); diff --git a/packages/vtable/src/scenegraph/layout/update-width.ts b/packages/vtable/src/scenegraph/layout/update-width.ts index ac06b5a68e..55d156e3dd 100644 --- a/packages/vtable/src/scenegraph/layout/update-width.ts +++ b/packages/vtable/src/scenegraph/layout/update-width.ts @@ -17,6 +17,14 @@ import { getProp } from '../utils/get-prop'; import { isMergeCellGroup } from '../utils/is-merge-cell-group'; import { getQuadProps } from '../utils/padding'; import { updateCellContentWidth } from '../utils/text-icon-layout'; +import { + type CornerCustomMergeRangeUpdateMap, + createCornerCustomMergeContainer, + flushCornerCustomMergeContentUpdates, + isCornerCustomMergeRange, + queueCornerCustomMergeContentUpdate, + shouldRenderCornerCustomMergeContent +} from '../utils/corner-custom-merge'; import { computeRowHeight } from './compute-row-height'; import { updateCellHeightForRow } from './update-height'; import { getHierarchyOffset } from '../utils/get-hierarchy-offset'; @@ -31,7 +39,16 @@ import type { ColumnDefine, ListTableConstructorOptions } from '../../ts-types'; * @param {number} detaX * @return {*} */ -export function updateColWidth(scene: Scenegraph, col: number, detaX: number, skipTableWidthMap?: boolean) { +export function updateColWidth( + scene: Scenegraph, + col: number, + detaX: number, + skipTableWidthMap?: boolean, + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap +) { + const shouldFlushCornerCustomMergeRanges = !pendingCornerCustomMergeRanges; + pendingCornerCustomMergeRanges ??= new Map(); + if (!skipTableWidthMap) { scene.table._setColWidth(col, scene.table.getColWidth(col) + detaX, true); } @@ -40,33 +57,33 @@ export function updateColWidth(scene: Scenegraph, col: number, detaX: number, sk const colOrCornerHeaderColumn = scene.getColGroup(col, true) as Group; const rightTopColumn = scene.getColGroupInRightTopCorner(col); if (colOrCornerHeaderColumn && !rightTopColumn) { - updateColunmWidth(colOrCornerHeaderColumn, detaX, 'col-corner', scene); + updateColunmWidth(colOrCornerHeaderColumn, detaX, 'col-corner', scene, pendingCornerCustomMergeRanges); } // deal with right bottom frozen cells if (rightTopColumn) { - updateColunmWidth(rightTopColumn, detaX, 'right-top', scene); + updateColunmWidth(rightTopColumn, detaX, 'right-top', scene, pendingCornerCustomMergeRanges); } // deal with row header or body or right frozen cells const rowHeaderOrBodyColumn = scene.getColGroup(col) as Group; if (rowHeaderOrBodyColumn) { - updateColunmWidth(rowHeaderOrBodyColumn, detaX, 'row-body', scene); + updateColunmWidth(rowHeaderOrBodyColumn, detaX, 'row-body', scene, pendingCornerCustomMergeRanges); } const leftBottomColumn = scene.getColGroupInLeftBottomCorner(col); // deal with left bottom frozen cells if (leftBottomColumn) { - updateColunmWidth(leftBottomColumn, detaX, 'left-bottom', scene); + updateColunmWidth(leftBottomColumn, detaX, 'left-bottom', scene, pendingCornerCustomMergeRanges); } // deal with bottom frozen cells const bottomColumn = scene.getColGroupInBottom(col); if (bottomColumn) { - updateColunmWidth(bottomColumn, detaX, 'bottom', scene); + updateColunmWidth(bottomColumn, detaX, 'bottom', scene, pendingCornerCustomMergeRanges); } // deal with right bottom frozen cells const rightBottomColumn = scene.getColGroupInRightBottomCorner(col); if (rightBottomColumn) { - updateColunmWidth(rightBottomColumn, detaX, 'right-bottom', scene); + updateColunmWidth(rightBottomColumn, detaX, 'right-bottom', scene, pendingCornerCustomMergeRanges); } // 更新剩余列位置 @@ -116,6 +133,10 @@ export function updateColWidth(scene: Scenegraph, col: number, detaX: number, sk }); } + if (shouldFlushCornerCustomMergeRanges) { + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, scene.table); + } + // scene.table.setColWidth(col, rowHeaderOrBodyColumn.attribute.width, true); } @@ -124,7 +145,8 @@ function updateColunmWidth( detaX: number, // autoRowHeight: boolean, mode: 'col-corner' | 'row-body' | 'bottom' | 'left-bottom' | 'right-top' | 'right-bottom', - scene: Scenegraph + scene: Scenegraph, + pendingCornerCustomMergeRanges: CornerCustomMergeRangeUpdateMap ) { let needRerangeRow = false; // const colOrCornerHeaderColumn = scene.getColGroup(col, true) as Group; @@ -141,7 +163,8 @@ function updateColunmWidth( oldColumnWidth + detaX, detaX, mode === 'row-body' ? cell.col < scene.table.rowHeaderLevelCount : true, - scene.table.internalProps.autoWrapText + scene.table.internalProps.autoWrapText, + pendingCornerCustomMergeRanges ); if (isHeightChange) { const mergeInfo = getCellMergeInfo(scene.table, cell.col, cell.row); @@ -246,7 +269,8 @@ function updateCellWidth( detaX: number, isHeader: boolean, // autoColWidth: boolean, - autoWrapText: boolean + autoWrapText: boolean, + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap ): boolean { if (cell.attribute.width === distWidth && !cell.needUpdateWidth) { return false; @@ -308,7 +332,16 @@ function updateCellWidth( oldBarCell.release(); // deal width text - const cellChange = updateMergeCellContentWidth(cellGroup, distWidth, detaX, autoRowHeight, true, scene.table); + const cellChange = updateMergeCellContentWidth( + cellGroup, + distWidth, + detaX, + autoRowHeight, + true, + scene.table, + true, + pendingCornerCustomMergeRanges + ); isHeightChange = isHeightChange || cellChange; } else if (type === 'sparkline') { // 目前先采用重新生成节点的方案 @@ -362,9 +395,15 @@ function updateCellWidth( (cell.getChildByName(CUSTOM_MERGE_CONTAINER_NAME) as Group); if (customContainer) { // fix for custom component flash - // if (scene.table.reactCustomLayout) { - // scene.table.reactCustomLayout.removeCustomCell(col, row); - // } + if (scene.table.reactCustomLayout) { + const mergeRange = isMergeCellGroup(cellGroup) + ? scene.table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow) + : undefined; + const shouldUseMergeStart = mergeRange && isCornerCustomMergeRange(mergeRange, scene.table); + const removeCol = shouldUseMergeStart ? mergeRange.start.col : col; + const removeRow = shouldUseMergeStart ? mergeRange.start.row : row; + scene.table.reactCustomLayout.removeCustomCell(removeCol, removeRow); + } // customContainer.removeAllChild(); let customElementsGroup; cell.removeChild(customContainer); @@ -379,8 +418,10 @@ function updateCellWidth( const customContainer = (mergedCell.getChildByName(CUSTOM_CONTAINER_NAME) as Group) || (mergedCell.getChildByName(CUSTOM_MERGE_CONTAINER_NAME) as Group); - customContainer.removeAllChild(); - mergedCell.removeChild(customContainer); + if (customContainer) { + customContainer.removeAllChild(); + mergedCell.removeChild(customContainer); + } getCustomCellMergeCustom(mergeCol, row, mergedCell, scene.table); } } else { @@ -411,7 +452,13 @@ function updateCellWidth( } } - if (customLayout || customRender) { + const mergeRange = isMergeCellGroup(cellGroup) + ? scene.table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow) + : undefined; + const shouldRenderContent = shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table); + if (!shouldRenderContent) { + renderDefault = false; + } else if (customLayout || customRender) { // const { autoRowHeight } = table.internalProps; const style = scene.table._getCellStyle(col, row) as ProgressBarStyle; const padding = getQuadProps(getProp('padding', style, col, row, scene.table)); @@ -421,25 +468,32 @@ function updateCellWidth( width = scene.table.getColsWidth(cellGroup.mergeStartCol, cellGroup.mergeEndCol); height = scene.table.getRowsHeight(cellGroup.mergeStartRow, cellGroup.mergeEndRow); } + let customCol = col; + let customRow = row; + if (mergeRange && isCornerCustomMergeRange(mergeRange, scene.table)) { + customCol = mergeRange.start.col; + customRow = mergeRange.start.row; + } const customResult = dealWithCustom( customLayout, customRender, - col, - row, + customCol, + customRow, width, height, false, scene.table.isAutoRowHeight(row), padding, - isMergeCellGroup(cellGroup) - ? { - start: { col: cellGroup.mergeStartCol, row: cellGroup.mergeStartRow }, - end: { col: cellGroup.mergeEndCol, row: cellGroup.mergeEndRow } - } - : undefined, + mergeRange, + scene.table + ); + customElementsGroup = createCornerCustomMergeContainer( + customResult.elementsGroup, + width, + height, + mergeRange, scene.table ); - customElementsGroup = customResult.elementsGroup; renderDefault = customResult.renderDefault; isHeightChange = true; } @@ -449,6 +503,12 @@ function updateCellWidth( } else if (customElementsGroup) { cell.appendChild(customElementsGroup); } + if (isMergeCellGroup(cellGroup)) { + const mergeRange = scene.table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); + if (isCornerCustomMergeRange(mergeRange, scene.table)) { + cell.setAttribute('clip', !shouldRenderCornerCustomMergeContent(col, row, mergeRange, scene.table)); + } + } } } const cellChange = updateMergeCellContentWidth( @@ -457,7 +517,9 @@ function updateCellWidth( detaX, autoRowHeight, renderDefault, - scene.table + scene.table, + !customContainer, + pendingCornerCustomMergeRanges ); isHeightChange = isHeightChange || cellChange; } @@ -474,7 +536,9 @@ function updateMergeCellContentWidth( detaX: number, autoRowHeight: boolean, renderDefault: boolean, - table: BaseTableAPI + table: BaseTableAPI, + refreshCornerCustomMergeContent = true, + pendingCornerCustomMergeRanges?: CornerCustomMergeRangeUpdateMap ) { if (isMergeCellGroup(cellGroup)) { distWidth = 0; @@ -567,6 +631,10 @@ function updateMergeCellContentWidth( isHeightChange = isHeightChange || changed; } } + const mergeRange = table.getCellRange(cellGroup.mergeStartCol, cellGroup.mergeStartRow); + if (refreshCornerCustomMergeContent && isCornerCustomMergeRange(mergeRange, table)) { + queueCornerCustomMergeContentUpdate(mergeRange, table, pendingCornerCustomMergeRanges); + } return isHeightChange; } diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index fb43b590b6..f2a18bbad8 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -512,6 +512,8 @@ export class Scenegraph { } releaseStage() { + this.clear = true; + this._needUpdateContainer = false; const releaseAppRef = this.releaseVRenderAppRef; this.releaseVRenderAppRef = undefined; @@ -994,8 +996,14 @@ export class Scenegraph { * @param {number} detaX 改变的宽度值 * @return {*} */ - updateColWidth(col: number, detaX: number, skipUpdateContainer?: boolean, skipTableWidthMap?: boolean) { - updateColWidth(this, col, Math.round(detaX), skipTableWidthMap); + updateColWidth( + col: number, + detaX: number, + skipUpdateContainer?: boolean, + skipTableWidthMap?: boolean, + pendingCornerCustomMergeRanges?: Map + ) { + updateColWidth(this, col, Math.round(detaX), skipTableWidthMap, pendingCornerCustomMergeRanges); // this.updateContainerWidth(col, detaX); if (!skipUpdateContainer) { // this.updateContainerAttrWidthAndX(); @@ -1538,9 +1546,14 @@ export class Scenegraph { } } - updateRowHeight(row: number, detaY: number, skipTableHeightMap?: boolean) { + updateRowHeight( + row: number, + detaY: number, + skipTableHeightMap?: boolean, + pendingCornerCustomMergeRanges?: Map + ) { detaY = Math.round(detaY); - updateRowHeight(this, row, detaY, skipTableHeightMap); + updateRowHeight(this, row, detaY, skipTableHeightMap, pendingCornerCustomMergeRanges); this.updateContainerHeight(row, detaY); } updateRowsHeight(rows: number[], detaYs: number[], skipTableHeightMap?: boolean) { @@ -2223,6 +2236,10 @@ export class Scenegraph { if (!this._needUpdateContainer) { this._needUpdateContainer = true; setTimeout(() => { + if (!this._needUpdateContainer || this.clear) { + this._needUpdateContainer = false; + return; + } this.updateContainerSync(updateConfig.needUpdateCellY ?? false); }, 0); } diff --git a/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts new file mode 100644 index 0000000000..434eac5545 --- /dev/null +++ b/packages/vtable/src/scenegraph/utils/corner-custom-merge.ts @@ -0,0 +1,111 @@ +import type { Group as VGroup } from '@src/vrender'; +import type { CellRange } from '../../ts-types'; +import type { BaseTableAPI } from '../../ts-types/base-table'; +import { CUSTOM_CONTAINER_NAME, CUSTOM_MERGE_CONTAINER_NAME } from '../component/custom'; +import { Group } from '../graphic/group'; + +export type CornerCustomMergeRangeUpdateMap = Map; + +export function isCornerCustomMergeRange(range: CellRange | undefined, table: BaseTableAPI): boolean { + return ( + !!range?.isCustom && + (range.start.col !== range.end.col || range.start.row !== range.end.row) && + table.isCornerHeader(range.start.col, range.start.row) && + table.isCornerHeader(range.end.col, range.end.row) + ); +} + +export function shouldRenderCornerCustomMergeContent( + col: number, + row: number, + range: CellRange | undefined, + table: BaseTableAPI +): boolean { + return !isCornerCustomMergeRange(range, table) || (col === range.end.col && row === range.end.row); +} + +export function createCornerCustomMergeContainer( + customElementsGroup: VGroup | undefined, + width: number, + height: number, + range: CellRange | undefined, + table: BaseTableAPI +): VGroup | undefined { + if (!customElementsGroup || !isCornerCustomMergeRange(range, table)) { + return customElementsGroup; + } + + const customContainer = new Group({ + x: 0, + y: 0, + width, + height, + fill: false, + stroke: false, + pickable: false, + clip: true + }); + customContainer.name = CUSTOM_CONTAINER_NAME; + customContainer.col = range.start.col; + customContainer.row = range.start.row; + customContainer.appendChild(customElementsGroup); + + return customContainer as unknown as VGroup; +} + +export function updateCornerCustomMergeContent(range: CellRange, table: BaseTableAPI): void { + refreshCornerCustomMergeContent(range, table); +} + +export function queueCornerCustomMergeContentUpdate( + range: CellRange, + table: BaseTableAPI, + pendingRanges?: CornerCustomMergeRangeUpdateMap +): void { + if (!pendingRanges) { + refreshCornerCustomMergeContent(range, table); + return; + } + + pendingRanges.set(getCornerCustomMergeRangeKey(range), range); +} + +export function flushCornerCustomMergeContentUpdates( + pendingRanges: CornerCustomMergeRangeUpdateMap, + table: BaseTableAPI +): void { + pendingRanges.forEach(range => { + refreshCornerCustomMergeContentForResize(range, table); + }); + pendingRanges.clear(); +} + +function getCornerCustomMergeRangeKey(range: CellRange): string { + return `${range.start.col}-${range.start.row}-${range.end.col}-${range.end.row}`; +} + +function refreshCornerCustomMergeContent(range: CellRange, table: BaseTableAPI): void { + const cellGroup = table.scenegraph.getCell(range.end.col, range.end.row); + const customContainer = + cellGroup.getChildByName(CUSTOM_CONTAINER_NAME) || cellGroup.getChildByName(CUSTOM_MERGE_CONTAINER_NAME); + + if (customContainer) { + table.reactCustomLayout?.removeCustomCell(range.start.col, range.start.row); + customContainer.removeAllChild(); + cellGroup.removeChild(customContainer); + } + + table.scenegraph.updateCellContent(range.end.col, range.end.row); +} + +function refreshCornerCustomMergeContentForResize(range: CellRange, table: BaseTableAPI): void { + const cellGroup = table.scenegraph.getCell(range.end.col, range.end.row); + const customContainer = + cellGroup.getChildByName(CUSTOM_CONTAINER_NAME) || cellGroup.getChildByName(CUSTOM_MERGE_CONTAINER_NAME); + + if (customContainer) { + cellGroup.removeChild(customContainer); + } + + table.scenegraph.updateCellContent(range.end.col, range.end.row); +} diff --git a/packages/vtable/src/state/resize/update-resize-column.ts b/packages/vtable/src/state/resize/update-resize-column.ts index 3c7add4245..7d7c7cf41e 100644 --- a/packages/vtable/src/state/resize/update-resize-column.ts +++ b/packages/vtable/src/state/resize/update-resize-column.ts @@ -1,5 +1,6 @@ import type { ListTable } from '../../ListTable'; import type { PivotHeaderLayoutMap } from '../../layout/pivot-header-layout'; +import { flushCornerCustomMergeContentUpdates } from '../../scenegraph/utils/corner-custom-merge'; import type { IndicatorData } from '../../ts-types/list-table/layout-map/api'; import type { StateManager } from '../state'; @@ -133,26 +134,47 @@ export function updateResizeColumn(xInTable: number, yInTable: number, state: St } function updateResizeColForColumn(detaX: number, state: StateManager) { + const pendingCornerCustomMergeRanges = new Map(); if (state.table.widthMode === 'adaptive' && state.columnResize.col < state.table.colCount - 1) { // in adaptive mode, the right column width can not be negative // const rightColWidth = state.table.getColWidth(state.columnResize.col + 1); // if (rightColWidth - detaX < 0) { // detaX = rightColWidth; // } - state.table.scenegraph.updateColWidth(state.columnResize.col, detaX); - state.table.scenegraph.updateColWidth(state.columnResize.col + 1, -detaX); + state.table.scenegraph.updateColWidth( + state.columnResize.col, + detaX, + undefined, + undefined, + pendingCornerCustomMergeRanges + ); + state.table.scenegraph.updateColWidth( + state.columnResize.col + 1, + -detaX, + undefined, + undefined, + pendingCornerCustomMergeRanges + ); state.table.internalProps._widthResizedColMap.add(state.columnResize.col); state.table.internalProps._widthResizedColMap.add(state.columnResize.col + 1); } else { - state.table.scenegraph.updateColWidth(state.columnResize.col, detaX); + state.table.scenegraph.updateColWidth( + state.columnResize.col, + detaX, + undefined, + undefined, + pendingCornerCustomMergeRanges + ); state.table.internalProps._widthResizedColMap.add(state.columnResize.col); } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } function updateResizeColForAll(detaX: number, state: StateManager) { // 全列调整 const layout = state.table.internalProps.layoutMap as PivotHeaderLayoutMap; + const pendingCornerCustomMergeRanges = new Map(); for (let col = state.table.frozenColCount; col < state.table.colCount - state.table.rightFrozenColCount; col++) { // 是否禁止调整列宽disableColumnResize 对应canResizeColumn的逻辑判断 if (!(state.table.internalProps.transpose || (state.table.isPivotTable() && !layout.indicatorsAsCol))) { @@ -162,9 +184,10 @@ function updateResizeColForAll(detaX: number, state: StateManager) { } } // state.table.setColWidth(col, afterSize); - state.table.scenegraph.updateColWidth(col, detaX); + state.table.scenegraph.updateColWidth(col, detaX, undefined, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._widthResizedColMap.add(col); } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } function updateResizeColForIndicator(detaX: number, state: StateManager) { @@ -180,20 +203,22 @@ function updateResizeColForIndicator(detaX: number, state: StateManager) { resizeDimensionKey = headerPath.dimensionKey; resizeDimensionValue = headerPath.value; } + const pendingCornerCustomMergeRanges = new Map(); for (let col = state.table.rowHeaderLevelCount; col < state.table.colCount - state.table.rightFrozenColCount; col++) { const indicatorKey = layout.getIndicatorKey(col, state.table.columnHeaderLevelCount); if (layout.indicatorsAsCol && indicatorKey === resizeIndicatorKey) { - state.table.scenegraph.updateColWidth(col, detaX); + state.table.scenegraph.updateColWidth(col, detaX, undefined, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._widthResizedColMap.add(col); } else if (!layout.indicatorsAsCol) { const headerPaths = layout.getCellHeaderPaths(col, state.table.columnHeaderLevelCount - 1); const headerPath = headerPaths?.colHeaderPaths[headerPaths.colHeaderPaths.length - 1]; if (headerPath && resizeDimensionKey === headerPath.dimensionKey && resizeDimensionValue === headerPath.value) { - state.table.scenegraph.updateColWidth(col, detaX); + state.table.scenegraph.updateColWidth(col, detaX, undefined, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._widthResizedColMap.add(col); } } } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } function updateResizeColForIndicatorGroup(detaX: number, state: StateManager) { @@ -210,6 +235,7 @@ function updateResizeColForIndicatorGroup(detaX: number, state: StateManager) { // 计算当前受影响列的总宽度 后面会利用这个计算比例 const totalColWidth = state.table.getColsWidth(startCol, endCol); const moveX = detaX; // 纠正moveX 用于指标列均分该值 + const pendingCornerCustomMergeRanges = new Map(); for (let col = startCol; col <= endCol; col++) { // 是否禁止调整列宽disableColumnResize 对应canResizeColumn的逻辑判断 if (!(state.table.internalProps.transpose || (state.table.isPivotTable() && !layout.indicatorsAsCol))) { @@ -227,7 +253,8 @@ function updateResizeColForIndicatorGroup(detaX: number, state: StateManager) { } else if (deltaWidth < 0 && deltaWidth >= -0.5) { deltaWidth = -0.5; } - state.table.scenegraph.updateColWidth(col, deltaWidth); + state.table.scenegraph.updateColWidth(col, deltaWidth, undefined, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._widthResizedColMap.add(col); } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } diff --git a/packages/vtable/src/state/resize/update-resize-row.ts b/packages/vtable/src/state/resize/update-resize-row.ts index 3622b6f58d..0d61f43790 100644 --- a/packages/vtable/src/state/resize/update-resize-row.ts +++ b/packages/vtable/src/state/resize/update-resize-row.ts @@ -1,5 +1,6 @@ import type { ListTable } from '../../ListTable'; import type { PivotHeaderLayoutMap } from '../../layout/pivot-header-layout'; +import { flushCornerCustomMergeContentUpdates } from '../../scenegraph/utils/corner-custom-merge'; import type { IndicatorData } from '../../ts-types/list-table/layout-map/api'; import type { StateManager } from '../state'; @@ -69,21 +70,24 @@ export function updateResizeRow(xInTable: number, yInTable: number, state: State } function updateResizeColForRow(detaY: number, state: StateManager) { + const pendingCornerCustomMergeRanges = new Map(); if (state.table.heightMode === 'adaptive' && state.rowResize.row < state.table.rowCount - 1) { - state.table.scenegraph.updateRowHeight(state.rowResize.row, detaY); - state.table.scenegraph.updateRowHeight(state.rowResize.row + 1, -detaY); + state.table.scenegraph.updateRowHeight(state.rowResize.row, detaY, undefined, pendingCornerCustomMergeRanges); + state.table.scenegraph.updateRowHeight(state.rowResize.row + 1, -detaY, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(state.rowResize.row); state.table.internalProps._heightResizedRowMap.add(state.rowResize.row + 1); } else { - state.table.scenegraph.updateRowHeight(state.rowResize.row, detaY); + state.table.scenegraph.updateRowHeight(state.rowResize.row, detaY, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(state.rowResize.row); } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } function updateResizeColForAll(detaY: number, state: StateManager) { // 全列调整 // const layout = state.table.internalProps.layoutMap as PivotHeaderLayoutMap; + const pendingCornerCustomMergeRanges = new Map(); for (let row = state.table.frozenRowCount; row < state.table.rowCount - state.table.bottomFrozenRowCount; row++) { // // 是否禁止调整列宽disableRowResize 对应canResizeRow的逻辑判断 // if (!(state.table.internalProps.transpose || (state.table.isPivotTable() && !layout.indicatorsAsCol))) { @@ -92,9 +96,10 @@ function updateResizeColForAll(detaY: number, state: StateManager) { // continue; // } // } - state.table.scenegraph.updateRowHeight(row, detaY); + state.table.scenegraph.updateRowHeight(row, detaY, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(row); } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } function updateResizeColForIndicator(detaY: number, state: StateManager) { @@ -110,6 +115,7 @@ function updateResizeColForIndicator(detaY: number, state: StateManager) { resizeDimensionKey = headerPath?.dimensionKey; resizeDimensionValue = headerPath?.value; } + const pendingCornerCustomMergeRanges = new Map(); for ( let row = state.table.columnHeaderLevelCount; row < state.table.rowCount - state.table.bottomFrozenRowCount; @@ -117,17 +123,18 @@ function updateResizeColForIndicator(detaY: number, state: StateManager) { ) { const indicatorKey = layout.getIndicatorKey(state.table.rowHeaderLevelCount, row); if (!layout.indicatorsAsCol && indicatorKey === resizeIndicatorKey) { - state.table.scenegraph.updateRowHeight(row, detaY); + state.table.scenegraph.updateRowHeight(row, detaY, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(row); } else if (layout.indicatorsAsCol) { const headerPaths = layout.getCellHeaderPaths(state.table.rowHeaderLevelCount - 1, row); const headerPath = headerPaths?.rowHeaderPaths?.[headerPaths.rowHeaderPaths.length - 1]; if (headerPath && resizeDimensionKey === headerPath.dimensionKey && resizeDimensionValue === headerPath.value) { - state.table.scenegraph.updateRowHeight(row, detaY); + state.table.scenegraph.updateRowHeight(row, detaY, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(row); } } } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); } function updateResizeColForIndicatorGroup(detaY: number, state: StateManager) { @@ -144,6 +151,7 @@ function updateResizeColForIndicatorGroup(detaY: number, state: StateManager) { // 计算当前受影响列的总宽度 后面会利用这个计算比例 const totalRowHeight = state.table.getRowsHeight(startRow, endRow); const moveY = detaY; // 纠正moveX 用于指标列均分该值 + const pendingCornerCustomMergeRanges = new Map(); for (let row = startRow; row <= endRow; row++) { // 是否禁止调整列宽disableRowResize 对应canResizeRow的逻辑判断 // if (!(state.table.internalProps.transpose || (state.table.isPivotTable() && !layout.indicatorsAsCol))) { @@ -161,7 +169,8 @@ function updateResizeColForIndicatorGroup(detaY: number, state: StateManager) { } else if (deltaHeight < 0 && deltaHeight >= -0.5) { deltaHeight = -0.51; } - state.table.scenegraph.updateRowHeight(row, deltaHeight); + state.table.scenegraph.updateRowHeight(row, deltaHeight, undefined, pendingCornerCustomMergeRanges); state.table.internalProps._heightResizedRowMap.add(row); } + flushCornerCustomMergeContentUpdates(pendingCornerCustomMergeRanges, state.table); }