diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 34428165fe..29457b6827 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: cache-dependency-path: './common/config/rush/pnpm-lock.yaml' - name: Update npm - run: npm install -g npm@latest + run: npm install -g npm@11.18.0 - name: Install latest pnpm run: npm install -g pnpm@10.7.0 diff --git a/.gitignore b/.gitignore index 71ca08ddf2..83c3ca4718 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,6 @@ tsconfig.tsbuildinfo .history .trae/ .agents/ + +# codex +.omx/ diff --git a/common/changes/@visactor/vtable-gantt/fix-issue-4320-list-table-event-type_2026-06-25-15-59.json b/common/changes/@visactor/vtable-gantt/fix-issue-4320-list-table-event-type_2026-06-25-15-59.json new file mode 100644 index 0000000000..34ad37b50d --- /dev/null +++ b/common/changes/@visactor/vtable-gantt/fix-issue-4320-list-table-event-type_2026-06-25-15-59.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: export table event type from gantt package", + "type": "patch", + "packageName": "@visactor/vtable-gantt" + } + ], + "packageName": "@visactor/vtable-gantt", + "email": "biukam.w@gmail.com" +} diff --git a/common/changes/@visactor/vtable-gantt/fix-issue-5159-gantt-sort-scales_2026-06-26-12-00.json b/common/changes/@visactor/vtable-gantt/fix-issue-5159-gantt-sort-scales_2026-06-26-12-00.json new file mode 100644 index 0000000000..7f44ce2b89 --- /dev/null +++ b/common/changes/@visactor/vtable-gantt/fix-issue-5159-gantt-sort-scales_2026-06-26-12-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable-gantt", + "comment": "Fix a crash in the Gantt component when `zoomScale` is configured without `timelineHeader.scales`, and initialize timeline scales from the active zoom level or a safe default (GitHub #5159)", + "type": "patch" + } + ], + "packageName": "@visactor/vtable-gantt", + "email": "53095992+Jian-Zhang08@users.noreply.github.com" +} diff --git a/common/changes/@visactor/vtable-sheet/fix-formula-code-execution_2026-07-15-10-53.json b/common/changes/@visactor/vtable-sheet/fix-formula-code-execution_2026-07-15-10-53.json new file mode 100644 index 0000000000..edb511304b --- /dev/null +++ b/common/changes/@visactor/vtable-sheet/fix-formula-code-execution_2026-07-15-10-53.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable-sheet", + "comment": "fix: prevent formula arithmetic evaluation from executing user-controlled cell content", + "type": "patch" + } + ], + "packageName": "@visactor/vtable-sheet", + "email": "892739385@qq.com" +} diff --git a/common/config/jest/vrender-module-name-mapper.js b/common/config/jest/vrender-module-name-mapper.js new file mode 100644 index 0000000000..f28d34811f --- /dev/null +++ b/common/config/jest/vrender-module-name-mapper.js @@ -0,0 +1,45 @@ +function createVRenderModuleNameMapper(nodeModulesRoot = '/node_modules') { + const visactorRoot = `${nodeModulesRoot}/@visactor`; + + return { + '^@visactor/vrender/entries/(.*)$': `${visactorRoot}/vrender/cjs/entries/$1.js`, + '^@visactor/vrender-core/color-string$': `${visactorRoot}/vrender-core/cjs/color-string/index.js`, + '^@visactor/vrender-core/event/constant$': `${visactorRoot}/vrender-core/cjs/event/public-constant.js`, + '^@visactor/vrender-core/graphic/builtin-symbol$': + `${visactorRoot}/vrender-core/cjs/graphic/builtin-symbol.js`, + '^@visactor/vrender-core/render/draw-interceptor$': + `${visactorRoot}/vrender-core/cjs/render/contributions/render/draw-interceptor.js`, + '^@visactor/vrender-core/render/symbol$': + `${visactorRoot}/vrender-core/cjs/render/contributions/render/symbol.js`, + '^@visactor/vrender-core/(.*)$': `${visactorRoot}/vrender-core/cjs/$1.js`, + '^@visactor/vrender-kits/env$': `${visactorRoot}/vrender-kits/cjs/env/index.js`, + '^@visactor/vrender-kits/event/extension$': + `${visactorRoot}/vrender-kits/cjs/event/extension/index.js`, + '^@visactor/vrender-kits/(.*)$': `${visactorRoot}/vrender-kits/cjs/$1.js`, + '^@visactor/vrender-components/axis$': + `${visactorRoot}/vrender-components/cjs/axis/index.js`, + '^@visactor/vrender-components/axis/tick-data$': + `${visactorRoot}/vrender-components/cjs/axis/tick-data/index.js`, + '^@visactor/vrender-components/crosshair$': + `${visactorRoot}/vrender-components/cjs/crosshair/index.js`, + '^@visactor/vrender-components/label$': + `${visactorRoot}/vrender-components/cjs/label/index.js`, + '^@visactor/vrender-components/legend/discrete$': + `${visactorRoot}/vrender-components/cjs/legend/discrete/index.js`, + '^@visactor/vrender-components/poptip$': + `${visactorRoot}/vrender-components/cjs/poptip/index.js`, + '^@visactor/vrender-components/tag$': `${visactorRoot}/vrender-components/cjs/tag/index.js`, + '^@visactor/vrender-components/tooltip$': + `${visactorRoot}/vrender-components/cjs/tooltip/index.js`, + '^@visactor/vrender-components/util$': `${visactorRoot}/vrender-components/cjs/util/index.js`, + '^@visactor/vrender-components/(.*)$': `${visactorRoot}/vrender-components/cjs/$1.js`, + '^@visactor/vrender-animate/component$': + `${visactorRoot}/vrender-animate/cjs/component/index.js`, + '^@visactor/vrender-animate/state$': `${visactorRoot}/vrender-animate/cjs/state/index.js`, + '^@visactor/vrender-animate/(.*)$': `${visactorRoot}/vrender-animate/cjs/$1.js` + }; +} + +module.exports = { + createVRenderModuleNameMapper +}; diff --git a/common/config/rush/pnpm-config.json b/common/config/rush/pnpm-config.json index 198ea2f758..e388bf054c 100644 --- a/common/config/rush/pnpm-config.json +++ b/common/config/rush/pnpm-config.json @@ -82,9 +82,14 @@ * * PNPM documentation: https://pnpm.io/package_json#pnpmoverrides */ - // "globalOverrides": { - // "@visactor/vrender": "0.12.3" - // }, + "globalOverrides": { + "@visactor/vchart": "2.1.3", + "@visactor/vrender": "1.1.4", + "@visactor/vrender-core": "1.1.4", + "@visactor/vrender-kits": "1.1.4", + "@visactor/vrender-components": "1.1.4", + "@visactor/vrender-animate": "1.1.4" + }, /** * The `globalPeerDependencyRules` setting provides various settings for suppressing validation errors * that are reported during installation with `strictPeerDependencies=true`. The settings are copied @@ -188,4 +193,4 @@ // }, // "pnpm": { "futurePnpmFeature": true } } -} \ No newline at end of file +} diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 1d807dbfd5..e7322bd7a6 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -4,6 +4,14 @@ settings: autoInstallPeers: false excludeLinksFromLockfile: false +overrides: + '@visactor/vchart': 2.1.3 + '@visactor/vrender': 1.1.4 + '@visactor/vrender-core': 1.1.4 + '@visactor/vrender-kits': 1.1.4 + '@visactor/vrender-components': 1.1.4 + '@visactor/vrender-animate': 1.1.4 + pnpmfileChecksum: sha256-XTeZQwJtKk4dimqf7175GhJCXrnq3Yh7+kwb86Bwcdo= importers: @@ -19,34 +27,34 @@ importers: specifier: ^2.11.0 version: 2.57.0(vue@3.5.31(typescript@4.9.5)) '@visactor/openinula-vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../packages/openinula-vtable '@visactor/react-vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../packages/react-vtable '@visactor/vchart': - specifier: 2.0.13-alpha.10 - version: 2.0.13-alpha.10 + specifier: 2.1.3 + version: 2.1.3 '@visactor/vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../packages/vtable '@visactor/vtable-editors': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../packages/vtable-editors '@visactor/vtable-export': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../packages/vtable-export '@visactor/vtable-gantt': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../packages/vtable-gantt '@visactor/vtable-search': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../packages/vtable-search '@visactor/vtable-sheet': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../packages/vtable-sheet '@visactor/vue-vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../packages/vue-vtable '@visactor/vutils': specifier: ~1.0.17 @@ -140,7 +148,7 @@ importers: ../../packages/openinula-vtable: dependencies: '@visactor/vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -180,8 +188,8 @@ importers: specifier: 2019.6.4 version: 2019.6.4 '@visactor/vchart': - specifier: 2.0.13-alpha.10 - version: 2.0.13-alpha.10 + specifier: 2.1.3 + version: 2.1.3 axios: specifier: ^1.4.0 version: 1.14.0 @@ -270,7 +278,7 @@ importers: ../../packages/react-vtable: dependencies: '@visactor/vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -331,8 +339,8 @@ importers: specifier: 0.28.9 version: 0.28.9(@types/react@18.2.79) '@visactor/vchart': - specifier: 2.0.13-alpha.10 - version: 2.0.13-alpha.10 + specifier: 2.1.3 + version: 2.1.3 '@vitejs/plugin-react': specifier: 3.1.0 version: 3.1.0(vite@3.2.6(@types/node@25.5.0)(less@4.1.3)(sass@1.43.5)(terser@5.17.1)) @@ -429,23 +437,26 @@ importers: '@visactor/vdataset': specifier: ~1.0.17 version: 1.0.23 + '@visactor/vrender': + specifier: 1.1.4 + version: 1.1.4 '@visactor/vrender-animate': - specifier: ~1.0.41 - version: 1.0.44 + specifier: 1.1.4 + version: 1.1.4 '@visactor/vrender-components': - specifier: ~1.0.41 - version: 1.0.44 + specifier: 1.1.4 + version: 1.1.4 '@visactor/vrender-core': - specifier: ~1.0.41 - version: 1.0.44 + specifier: 1.1.4 + version: 1.1.4 '@visactor/vrender-kits': - specifier: ~1.0.41 - version: 1.0.44 + specifier: 1.1.4 + version: 1.1.4 '@visactor/vscale': specifier: ~1.0.17 version: 1.0.23 '@visactor/vtable-editors': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable-editors '@visactor/vutils': specifier: ~1.0.17 @@ -509,8 +520,8 @@ importers: specifier: 18.2.25 version: 18.2.25 '@visactor/vchart': - specifier: 2.0.13-alpha.10 - version: 2.0.13-alpha.10 + specifier: 2.1.3 + version: 2.1.3 '@vitejs/plugin-react': specifier: 3.1.0 version: 3.1.0(vite@3.2.6(@types/node@25.5.0)(less@4.1.3)(sass@1.43.5)(terser@5.17.1)) @@ -626,7 +637,7 @@ importers: ../../packages/vtable-calendar: dependencies: '@visactor/vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -849,7 +860,7 @@ importers: specifier: 2.0.7 version: 2.0.7 '@visactor/vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -904,8 +915,8 @@ importers: specifier: ^17.0.3 version: 17.0.7 '@visactor/vchart': - specifier: 2.0.13-alpha.10 - version: 2.0.13-alpha.10 + specifier: 2.1.3 + version: 2.1.3 '@vitejs/plugin-react': specifier: 3.1.0 version: 3.1.0(vite@3.2.6(@types/node@25.5.0)(less@4.1.3)(sass@1.43.5)(terser@5.17.1)) @@ -1009,10 +1020,10 @@ importers: specifier: ~1.0.17 version: 1.0.23 '@visactor/vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable '@visactor/vtable-editors': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable-editors '@visactor/vutils': specifier: ~1.0.17 @@ -1242,16 +1253,16 @@ importers: specifier: ^17.0.3 version: 17.0.7 '@visactor/vchart': - specifier: 2.0.13-alpha.10 - version: 2.0.13-alpha.10 + specifier: 2.1.3 + version: 2.1.3 '@visactor/vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable '@visactor/vtable-editors': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable-editors '@visactor/vtable-gantt': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable-gantt '@vitejs/plugin-react': specifier: 3.1.0 @@ -1350,7 +1361,7 @@ importers: ../../packages/vtable-search: dependencies: '@visactor/vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -1399,8 +1410,8 @@ importers: specifier: ^17.0.3 version: 17.0.7 '@visactor/vchart': - specifier: 2.0.13-alpha.10 - version: 2.0.13-alpha.10 + specifier: 2.1.3 + version: 2.1.3 '@vitejs/plugin-react': specifier: 3.1.0 version: 3.1.0(vite@3.2.6(@types/node@25.5.0)(less@4.1.3)(sass@1.43.5)(terser@5.17.1)) @@ -1504,13 +1515,13 @@ importers: specifier: ~1.0.17 version: 1.0.23 '@visactor/vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable '@visactor/vtable-editors': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable-editors '@visactor/vtable-plugins': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable-plugins '@visactor/vutils': specifier: ~1.0.17 @@ -1649,7 +1660,7 @@ importers: ../../packages/vue-vtable: dependencies: '@visactor/vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -1692,8 +1703,8 @@ importers: specifier: 2019.6.4 version: 2019.6.4 '@visactor/vchart': - specifier: 2.0.13-alpha.10 - version: 2.0.13-alpha.10 + specifier: 2.1.3 + version: 2.1.3 '@vitejs/plugin-vue': specifier: ^5.0.3 version: 5.2.4(vite@3.2.6(@types/node@25.5.0)(less@4.1.3)(sass@1.43.5)(terser@5.17.1))(vue@3.5.31(typescript@4.9.5)) @@ -1830,19 +1841,19 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vtable': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../../packages/vtable '@visactor/vtable-editors': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../../packages/vtable-editors '@visactor/vtable-gantt': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../../packages/vtable-gantt '@visactor/vtable-plugins': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../../packages/vtable-plugins '@visactor/vtable-sheet': - specifier: workspace:1.26.4 + specifier: workspace:1.26.5 version: link:../../packages/vtable-sheet devDependencies: '@internal/bundler': @@ -3553,8 +3564,8 @@ packages: '@ungap/promise-all-settled@1.1.2': resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} - '@visactor/vchart@2.0.13-alpha.10': - resolution: {integrity: sha512-fPIjs+cboQ5ORUz4YD6/0YTV5IFwuAlou9n/8zi2xShiFfolQQqUZ3hAlG+di7n1yXVJAVo8WOTi0LKx/rlCiA==} + '@visactor/vchart@2.1.3': + resolution: {integrity: sha512-3w+CJ367+lQxfFvnRVlU2RBOC3Ok/Tug8zdEFHp+26J9DPfdwkCb84llzcexsXV+e9kRoFH93XQEhJ49Q/lRxg==} '@visactor/vdataset@1.0.23': resolution: {integrity: sha512-zrLk9FBUWJoW6b30XnPKzXwAXl8USdLDfed6QZLsmdkylRU8V7yZeXE2aKwU8Lg1U4HmQngqmqOx7/QlbX44Tg==} @@ -3562,39 +3573,30 @@ packages: '@visactor/vlayouts@1.0.23': resolution: {integrity: sha512-fK1f5LmuumhYanLArk5yrT4BZxu4IAmdc8WMwfB/KAvV+2dTPFuBUMWbWnDl0siQoU9SX9l/bLozUnI9n7BwBQ==} - '@visactor/vrender-animate@1.0.37-alpha.1': - resolution: {integrity: sha512-jbaegMFAA8nWxWIGSis63kzongcupvJqxj3KHR4Zn9E9ZeK3NDYgccXck97EYz187/8NDft8Fu8k8bQBBioVxQ==} - - '@visactor/vrender-animate@1.0.44': - resolution: {integrity: sha512-MKtoaucmbQNJnuff5F6ws+c8+mRBz1gXVY/bHw38nfvSTZcsJcXYug2aDPf4V9XaUSValDnkx9TyLLbKJ0o0TA==} - - '@visactor/vrender-components@1.0.37-alpha.1': - resolution: {integrity: sha512-g0G+HRsepXwJExFqhGj/8NErwR84s5KI4quAMWPR0mwRVu1q+1l91TdMQgDxtriXVHrrLfmK3RdUmdOuhkMrtQ==} + '@visactor/vrender-animate@1.1.4': + resolution: {integrity: sha512-XBaCXMLSNw9E+htAEaqjby28mdINyVokNkLjKJ/ukmGobi3XX8siekXTNzCfe9M9UG4QGRTqyph848sxLB02Tw==} - '@visactor/vrender-components@1.0.44': - resolution: {integrity: sha512-vbCa/eME1UJL2iy0l4JHYJh1m6SG3rR5jt5umblLVRQjjBEWPAnhnPwQAA2TNiyO5eHG6qnyvcjgV5aeCeuJYw==} + '@visactor/vrender-components@1.1.4': + resolution: {integrity: sha512-N1bnIuefe6Lms7Ij8NGjKfNUYQpZ+M8RYsnjBR2So0wmnxAnXUTgqbpWDT9DiXzRF17qYSkF0HZJIdS/wN2o3Q==} - '@visactor/vrender-core@1.0.37-alpha.1': - resolution: {integrity: sha512-aSxF/FUuqM2HDj8DdvjdxvNqLBjW1f96ICdISXk9ZL/PoJVVZ8x9HHoESO5PDcMxLnkWGOSHhlSQJs7ON6A4gg==} + '@visactor/vrender-core@1.1.4': + resolution: {integrity: sha512-AyMKFohr/iK1Wc5jxvE/04MLcFI0VAR5qleDy+b2WwLi7UoeXn0tBhtzz72xIOKU1OrzvmflWsVmdnlh6ittwg==} - '@visactor/vrender-core@1.0.44': - resolution: {integrity: sha512-DrB+cg9//RkCZjchWConeXGOC99xwvEDdPfY7eEqajjPxUooJpSQ+pt51uD0VIwQCYtpsc2jVspefqdBh/e2Cg==} + '@visactor/vrender-kits@1.1.4': + resolution: {integrity: sha512-YD1y2TWh5yl6AzpW47HPcYogk0d6KntuwUrGocXqSkLXwzsGnYntE9RvAEGTu4ketdeahh+8/0w34d6BU0Hzig==} - '@visactor/vrender-kits@1.0.37-alpha.1': - resolution: {integrity: sha512-auQabc+V3413QeJOEujKGa/pCnuhzK2o4ogg8LEiArcXUKfKBvw9qS12Xo8G60diZMQe81kPle4LZJFD8j9FrA==} - - '@visactor/vrender-kits@1.0.44': - resolution: {integrity: sha512-B28zr+aTWlFIziH3b/ufUlpKQMv1Ie9ck8kTkHIGdBM/ES+CO6//GHMa/5OxuId19a9UhJxzj8cpeE/9mZLIoQ==} + '@visactor/vrender@1.1.4': + resolution: {integrity: sha512-+T09pS5EJ2HizxMCa3r8MuvgcWnoJpGUK2ykLZLcR6w+5kfpfbu2B8uQzetTOKn1CVIhDbuO05monfI7mCYGQA==} '@visactor/vscale@1.0.23': resolution: {integrity: sha512-XePhYuRoNAp+8MeSMuEOOvhVAlOwvM1sDT2yFxE6zdwVB2GjZk8mH+5N2xQGQWk75YmGJjlJASFtgwjlb1yWxw==} - '@visactor/vutils-extension@2.0.13-alpha.10': - resolution: {integrity: sha512-kjP0oGgJrtsfADAq9FMMSgU04VwIAF7WX4zOa0wdwv0cflpKVchX/d7DBHXL/720Ak+LygfsvIdOpawKOrZ/GA==} - '@visactor/vutils-extension@2.0.20': resolution: {integrity: sha512-now0AqIHimJr1/+7U9cJBkZ022VAsVlVANTN8mKLVIG1C5wnEYfKYG1Ew6ghjcSmkXi1c+pdrsAsj6i+ifRlwA==} + '@visactor/vutils-extension@2.1.3': + resolution: {integrity: sha512-i3NtsnAyf4RGn99irNRgF4zorjj0gFgta4no0DrTA0dD0jMbDe2R6lRc0ZFpIoKWJW1k6MG7kml6heVND/LiyQ==} + '@visactor/vutils@1.0.23': resolution: {integrity: sha512-M8SLqgdHhKN8QmQKTWD1gzEaHptpIV9pvMYvC6+VeOsqYvZZ6UdhSCAAczTYVo+m/uwcEC2JHSUspbrs8rzlRQ==} @@ -11176,17 +11178,18 @@ snapshots: '@ungap/promise-all-settled@1.1.2': {} - '@visactor/vchart@2.0.13-alpha.10': + '@visactor/vchart@2.1.3': dependencies: '@visactor/vdataset': 1.0.23 '@visactor/vlayouts': 1.0.23 - '@visactor/vrender-animate': 1.0.37-alpha.1 - '@visactor/vrender-components': 1.0.37-alpha.1 - '@visactor/vrender-core': 1.0.37-alpha.1 - '@visactor/vrender-kits': 1.0.37-alpha.1 + '@visactor/vrender': 1.1.4 + '@visactor/vrender-animate': 1.1.4 + '@visactor/vrender-components': 1.1.4 + '@visactor/vrender-core': 1.1.4 + '@visactor/vrender-kits': 1.1.4 '@visactor/vscale': 1.0.23 '@visactor/vutils': 1.0.23 - '@visactor/vutils-extension': 2.0.13-alpha.10 + '@visactor/vutils-extension': 2.1.3 '@visactor/vdataset@1.0.23': dependencies: @@ -11216,70 +11219,50 @@ snapshots: '@visactor/vutils': 1.0.23 eventemitter3: 4.0.7 - '@visactor/vrender-animate@1.0.37-alpha.1': + '@visactor/vrender-animate@1.1.4': dependencies: - '@visactor/vrender-core': 1.0.37-alpha.1 + '@visactor/vrender-core': 1.1.4 '@visactor/vutils': 1.0.23 - '@visactor/vrender-animate@1.0.44': + '@visactor/vrender-components@1.1.4': dependencies: - '@visactor/vrender-core': 1.0.44 - '@visactor/vutils': 1.0.23 - - '@visactor/vrender-components@1.0.37-alpha.1': - dependencies: - '@visactor/vrender-animate': 1.0.37-alpha.1 - '@visactor/vrender-core': 1.0.37-alpha.1 - '@visactor/vrender-kits': 1.0.37-alpha.1 + '@visactor/vrender-animate': 1.1.4 + '@visactor/vrender-core': 1.1.4 + '@visactor/vrender-kits': 1.1.4 '@visactor/vscale': 1.0.23 '@visactor/vutils': 1.0.23 - '@visactor/vrender-components@1.0.44': - dependencies: - '@visactor/vrender-animate': 1.0.44 - '@visactor/vrender-core': 1.0.44 - '@visactor/vrender-kits': 1.0.44 - '@visactor/vscale': 1.0.23 - '@visactor/vutils': 1.0.23 - - '@visactor/vrender-core@1.0.37-alpha.1': - dependencies: - '@visactor/vutils': 1.0.23 - color-convert: 2.0.1 - - '@visactor/vrender-core@1.0.44': + '@visactor/vrender-core@1.1.4': dependencies: '@visactor/vutils': 1.0.23 color-convert: 2.0.1 - '@visactor/vrender-kits@1.0.37-alpha.1': + '@visactor/vrender-kits@1.1.4': dependencies: '@resvg/resvg-js': 2.4.1 - '@visactor/vrender-core': 1.0.37-alpha.1 + '@visactor/vrender-core': 1.1.4 '@visactor/vutils': 1.0.23 gifuct-js: 2.1.2 lottie-web: 5.13.0 roughjs: 4.6.6 - '@visactor/vrender-kits@1.0.44': + '@visactor/vrender@1.1.4': dependencies: - '@resvg/resvg-js': 2.4.1 - '@visactor/vrender-core': 1.0.44 - '@visactor/vutils': 1.0.23 - gifuct-js: 2.1.2 - lottie-web: 5.13.0 - roughjs: 4.6.6 + '@visactor/vrender-animate': 1.1.4 + '@visactor/vrender-components': 1.1.4 + '@visactor/vrender-core': 1.1.4 + '@visactor/vrender-kits': 1.1.4 '@visactor/vscale@1.0.23': dependencies: '@visactor/vutils': 1.0.23 - '@visactor/vutils-extension@2.0.13-alpha.10': + '@visactor/vutils-extension@2.0.20': dependencies: '@visactor/vdataset': 1.0.23 '@visactor/vutils': 1.0.23 - '@visactor/vutils-extension@2.0.20': + '@visactor/vutils-extension@2.1.3': dependencies: '@visactor/vdataset': 1.0.23 '@visactor/vutils': 1.0.23 diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index bd4829a3fe..6f8ac0608d 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vtableMain","version":"1.26.4","mainProject":"@visactor/vtable","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vtableMain","version":"1.26.5","mainProject":"@visactor/vtable","nextBump":"patch"}] diff --git a/docs/assets/changelog/en/release.md b/docs/assets/changelog/en/release.md index 1c7dbc8273..19233703a5 100644 --- a/docs/assets/changelog/en/release.md +++ b/docs/assets/changelog/en/release.md @@ -1,3 +1,19 @@ +# v1.26.5 + +2026-07-15 + +**🐛 Bug Fixes** + +- **@visactor/vtable**: handle null table container + +**🔨 Chores** + +- **@visactor/vtable**: Render group title for single-column list tables. + +**Full Changelog**: https://github.com/VisActor/VTable/compare/v1.26.4...v1.26.5 + +[more detail about v1.26.5](https://github.com/VisActor/VTable/releases/tag/v1.26.5) + # v1.26.4 2026-07-06 diff --git a/docs/assets/changelog/zh/release.md b/docs/assets/changelog/zh/release.md index 82bb4fbecf..61e2327a9f 100644 --- a/docs/assets/changelog/zh/release.md +++ b/docs/assets/changelog/zh/release.md @@ -1,3 +1,19 @@ +# v1.26.5 + +2026-07-15 + +**🐛 问题修复** + +- **@visactor/vtable**: 处理表格容器为空的情况 + +**🔨 维护与杂项** + +- **@visactor/vtable**: 支持单列列表表格渲染分组标题 + +**Full Changelog**: https://github.com/VisActor/VTable/compare/v1.26.4...v1.26.5 + +[更多详情请查看 v1.26.5](https://github.com/VisActor/VTable/releases/tag/v1.26.5) + # v1.26.4 2026-07-06 diff --git a/docs/assets/demo-openinula/en/custom-layout/custom-layout.md b/docs/assets/demo-openinula/en/custom-layout/custom-layout.md index 41f07c68ad..3538e9516c 100644 --- a/docs/assets/demo-openinula/en/custom-layout/custom-layout.md +++ b/docs/assets/demo-openinula/en/custom-layout/custom-layout.md @@ -194,16 +194,14 @@ Inula.render( boundsPadding: [0, 0, 0, 10], cursor: 'pointer' }} - stateProxy={stateName => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { diff --git a/docs/assets/demo-openinula/zh/custom-layout/custom-layout.md b/docs/assets/demo-openinula/zh/custom-layout/custom-layout.md index 29fd783554..accae27d27 100644 --- a/docs/assets/demo-openinula/zh/custom-layout/custom-layout.md +++ b/docs/assets/demo-openinula/zh/custom-layout/custom-layout.md @@ -194,16 +194,14 @@ Inula.render( boundsPadding: [0, 0, 0, 10], cursor: 'pointer' }} - stateProxy={stateName => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { diff --git a/docs/assets/demo/en/custom-render/custom-cell-layout-jsx.md b/docs/assets/demo/en/custom-render/custom-cell-layout-jsx.md index a03e1cebd8..83c8e58c10 100644 --- a/docs/assets/demo/en/custom-render/custom-cell-layout-jsx.md +++ b/docs/assets/demo/en/custom-render/custom-cell-layout-jsx.md @@ -123,16 +123,14 @@ const option = { boundsPadding: [0, 0, 0, 10], cursor: 'pointer' }} - stateProxy={stateName => { - if (stateName === 'hover') { - return { - background: { - fill: '#c90', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#c90', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { diff --git a/docs/assets/demo/zh/custom-render/custom-cell-layout-jsx.md b/docs/assets/demo/zh/custom-render/custom-cell-layout-jsx.md index 246f656806..08ebefe696 100644 --- a/docs/assets/demo/zh/custom-render/custom-cell-layout-jsx.md +++ b/docs/assets/demo/zh/custom-render/custom-cell-layout-jsx.md @@ -123,16 +123,14 @@ const option = { boundsPadding: [0, 0, 0, 10], cursor: 'pointer' }} - stateProxy={stateName => { - if (stateName === 'hover') { - return { - background: { - fill: '#c90', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#c90', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { diff --git a/docs/assets/faq/en/34-How to implement text type buttons.md b/docs/assets/faq/en/34-How to implement text type buttons.md index 323fcbcc8d..38dce80876 100644 --- a/docs/assets/faq/en/34-How to implement text type buttons.md +++ b/docs/assets/faq/en/34-How to implement text type buttons.md @@ -50,12 +50,10 @@ import {createGroup, createText} from '@visactor/vtable/es/vrender'; underline: 0, cursor: 'pointer' }); - editText.stateProxy = (stateName: string) => { - if (stateName === 'hover') { - return { - fill: '#315efb', - underline: 1 - }; + editText.states = { + hover: { + fill: '#315efb', + underline: 1 } }; editText.addEventListener('mouseenter', e => { @@ -128,12 +126,10 @@ import {createGroup, createText} from '@visactor/vtable/es/vrender'; underline: 0, cursor: 'pointer' }); - editText.stateProxy = (stateName) => { - if (stateName === 'hover') { - return { - fill: '#315efb', - underline: 1 - }; + editText.states = { + hover: { + fill: '#315efb', + underline: 1 } }; editText.addEventListener('mouseenter', e => { @@ -158,12 +154,10 @@ import {createGroup, createText} from '@visactor/vtable/es/vrender'; underline: 0, cursor: 'pointer' }); - deleteText.stateProxy = (stateName) => { - if (stateName === 'hover') { - return { - fill: '#315efb', - underline: 1 - }; + deleteText.states = { + hover: { + fill: '#315efb', + underline: 1 } }; deleteText.addEventListener('mouseenter', e => { @@ -217,4 +211,3 @@ Related api: https://www.visactor.io/vtable/guide/custom_define/custom_layout - diff --git a/docs/assets/faq/en/56-VTable table component, using customLayout to customize the drawing elements, how to listen for mouse hover events on the elements, similar to the DOM's mouseenter event.md b/docs/assets/faq/en/56-VTable table component, using customLayout to customize the drawing elements, how to listen for mouse hover events on the elements, similar to the DOM's mouseenter event.md index 8f82f8d662..cf51f2ddda 100644 --- a/docs/assets/faq/en/56-VTable table component, using customLayout to customize the drawing elements, how to listen for mouse hover events on the elements, similar to the DOM's mouseenter event.md +++ b/docs/assets/faq/en/56-VTable table component, using customLayout to customize the drawing elements, how to listen for mouse hover events on the elements, similar to the DOM's mouseenter event.md @@ -103,16 +103,14 @@ const option = { boundsPadding: [0, 0, 0, 10], cursor: 'pointer' }} - stateProxy={stateName => { - if (stateName === 'hover') { - return { - background: { - fill: 'green', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: 'green', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onPointerEnter={event => { diff --git a/docs/assets/faq/zh/34-How to implement text type buttons.md b/docs/assets/faq/zh/34-How to implement text type buttons.md index 792468f1d7..a4731c7b9f 100644 --- a/docs/assets/faq/zh/34-How to implement text type buttons.md +++ b/docs/assets/faq/zh/34-How to implement text type buttons.md @@ -50,12 +50,10 @@ import {createGroup, createText} from '@visactor/vtable/es/vrender'; underline: 0, cursor: 'pointer' }); - editText.stateProxy = (stateName: string) => { - if (stateName === 'hover') { - return { - fill: '#315efb', - underline: 1 - }; + editText.states = { + hover: { + fill: '#315efb', + underline: 1 } }; editText.addEventListener('mouseenter', e => { @@ -128,12 +126,10 @@ import {createGroup, createText} from '@visactor/vtable/es/vrender'; underline: 0, cursor: 'pointer' }); - editText.stateProxy = (stateName) => { - if (stateName === 'hover') { - return { - fill: '#315efb', - underline: 1 - }; + editText.states = { + hover: { + fill: '#315efb', + underline: 1 } }; editText.addEventListener('mouseenter', e => { @@ -158,12 +154,10 @@ import {createGroup, createText} from '@visactor/vtable/es/vrender'; underline: 0, cursor: 'pointer' }); - deleteText.stateProxy = (stateName) => { - if (stateName === 'hover') { - return { - fill: '#315efb', - underline: 1 - }; + deleteText.states = { + hover: { + fill: '#315efb', + underline: 1 } }; deleteText.addEventListener('mouseenter', e => { @@ -217,4 +211,3 @@ import {createGroup, createText} from '@visactor/vtable/es/vrender'; github:https://github.com/VisActor/VTable
- diff --git a/docs/assets/faq/zh/56-VTable table component, using customLayout to customize the drawing elements, how to listen for mouse hover events on the elements, similar to the DOM's mouseenter event.md b/docs/assets/faq/zh/56-VTable table component, using customLayout to customize the drawing elements, how to listen for mouse hover events on the elements, similar to the DOM's mouseenter event.md index 6c7dff0f33..d36822a7b0 100644 --- a/docs/assets/faq/zh/56-VTable table component, using customLayout to customize the drawing elements, how to listen for mouse hover events on the elements, similar to the DOM's mouseenter event.md +++ b/docs/assets/faq/zh/56-VTable table component, using customLayout to customize the drawing elements, how to listen for mouse hover events on the elements, similar to the DOM's mouseenter event.md @@ -103,16 +103,14 @@ const option = { boundsPadding: [0, 0, 0, 10], cursor: 'pointer' }} - stateProxy={stateName => { - if (stateName === 'hover') { - return { - background: { - fill: 'green', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: 'green', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onPointerEnter={event => { diff --git a/docs/assets/guide/en/custom_define/custom_layout.md b/docs/assets/guide/en/custom_define/custom_layout.md index d808784ba6..84f14d5d7c 100644 --- a/docs/assets/guide/en/custom_define/custom_layout.md +++ b/docs/assets/guide/en/custom_define/custom_layout.md @@ -266,16 +266,14 @@ In addition to basic properties, state updates can be used to implement interact height: 20, cursor: 'pointer' }} - stateProxy={(stateName: string) => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { diff --git a/docs/assets/guide/en/sheet/formula.md b/docs/assets/guide/en/sheet/formula.md index 1919ba20f8..de0d9659e2 100644 --- a/docs/assets/guide/en/sheet/formula.md +++ b/docs/assets/guide/en/sheet/formula.md @@ -164,9 +164,10 @@ Supports common formula error handling: - Supports nested functions, such as `=IF(SUM(A1:A5)>100, MAX(B1:B5), MIN(C1:C5))` - Cell addresses use A1 format, such as A1, B2, C3, etc. - Range references use a colon separator, such as A1:B10 +- Arithmetic expressions are evaluated as numeric calculations only. Cell values or function results used with `+`, `-`, `*`, or `/` must be convertible to finite numbers. Non-numeric content returns a formula error and is never executed as script or code. **The formula capabilities are not fully complete** **The formula linkage processing, formula and sorting, and dragging rows and columns have conflicts** **If other cell editors are manually configured, the formula capabilities will be disabled.** **The formula capabilities will be gradually improved in future updates.** -**Welcome to participate in the contribution of the formula capabilities.** \ No newline at end of file +**Welcome to participate in the contribution of the formula capabilities.** diff --git a/docs/assets/guide/en/table_type/List_table/group_list.md b/docs/assets/guide/en/table_type/List_table/group_list.md index b293aaa5e6..18599e81d0 100644 --- a/docs/assets/guide/en/table_type/List_table/group_list.md +++ b/docs/assets/guide/en/table_type/List_table/group_list.md @@ -307,16 +307,14 @@ const option = { boundsPadding: [0, 0, 0, 10], cursor: 'pointer' }); - icon.stateProxy = (stateName) => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + icon.states = { + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }; icon.addEventListener('pointerenter', event => { @@ -472,4 +470,4 @@ If you don't want to associate the selected status of parent and child nodes, yo If you don't want to associate the selected status of the header and body cells, you can configure the `enableHeaderCheckboxCascade` property to enable or disable the header checkbox cascade function. -When enableCheckboxCascade is true, enableHeaderCheckboxCascade is true. When enableCheckboxCascade is false, enableHeaderCheckboxCascade is false. \ No newline at end of file +When enableCheckboxCascade is true, enableHeaderCheckboxCascade is true. When enableCheckboxCascade is false, enableHeaderCheckboxCascade is false. diff --git a/docs/assets/guide/zh/custom_define/custom_layout.md b/docs/assets/guide/zh/custom_define/custom_layout.md index c7c13fc208..9a10e4d09c 100644 --- a/docs/assets/guide/zh/custom_define/custom_layout.md +++ b/docs/assets/guide/zh/custom_define/custom_layout.md @@ -266,16 +266,14 @@ customLayout 函数返回一个对象,该对象需要有:`rootContainer`来 height: 20, cursor: 'pointer' }} - stateProxy={(stateName: string) => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { @@ -477,4 +475,4 @@ iconGroup.add(icon); const animation = icon.animate(); animation.setTimeline(table.animationManager.timeline); animation.to({ angle: 2 * Math.PI }, 1000, 'linear').loop(Infinity); -``` \ No newline at end of file +``` diff --git a/docs/assets/guide/zh/sheet/formula.md b/docs/assets/guide/zh/sheet/formula.md index 0aa77f2292..89696f84ba 100644 --- a/docs/assets/guide/zh/sheet/formula.md +++ b/docs/assets/guide/zh/sheet/formula.md @@ -161,8 +161,9 @@ VTableSheet 自身开发了 FormulaEngine 模块 作为核心的计算引擎: - 支持嵌套函数,如 `=IF(SUM(A1:A5)>100, MAX(B1:B5), MIN(C1:C5))` - 单元格地址使用A1格式,如A1, B2, C3等 - 区域引用使用冒号分隔,如A1:B10 +- 算术表达式只会按数值进行计算;参与 `+`、`-`、`*`、`/` 运算的单元格值或函数结果需要能转换为有效数字,非数值内容会返回公式错误,不会作为脚本或代码执行 **目前公式能力并不完善** **如公式的联动处理,公式和排序以及拖拽行列换位都有冲突** **手动配置了其他单元格编辑器editor后,公式能力会失效** -**后续会逐步完善这些功能,也欢迎有兴趣的开发者参与贡献** \ No newline at end of file +**后续会逐步完善这些功能,也欢迎有兴趣的开发者参与贡献** diff --git a/docs/assets/guide/zh/table_type/List_table/group_list.md b/docs/assets/guide/zh/table_type/List_table/group_list.md index 70f1027ecf..d665c3217d 100644 --- a/docs/assets/guide/zh/table_type/List_table/group_list.md +++ b/docs/assets/guide/zh/table_type/List_table/group_list.md @@ -311,16 +311,14 @@ const option = { boundsPadding: [0, 0, 0, 10], cursor: 'pointer' }); - icon.stateProxy = (stateName) => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + icon.states = { + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }; icon.addEventListener('pointerenter', event => { @@ -477,4 +475,3 @@ window['tableInstance'] = tableInstance; enableCheckboxCascade为true时则enableHeaderCheckboxCascade必定为true。 - diff --git a/docs/package.json b/docs/package.json index 752e0c7ddf..e05038b658 100644 --- a/docs/package.json +++ b/docs/package.json @@ -12,17 +12,17 @@ "dependencies": { "@arco-design/web-react": "2.66.12", "@arco-design/web-vue": "^2.11.0", - "@visactor/vtable": "workspace:1.26.4", - "@visactor/vtable-gantt": "workspace:1.26.4", - "@visactor/react-vtable": "workspace:1.26.4", - "@visactor/vue-vtable": "workspace:1.26.4", - "@visactor/openinula-vtable": "workspace:1.26.4", - "@visactor/vtable-editors": "workspace:1.26.4", - "@visactor/vtable-export": "workspace:1.26.4", - "@visactor/vtable-search": "workspace:1.26.4", - "@visactor/vtable-sheet": "workspace:1.26.4", + "@visactor/vtable": "workspace:1.26.5", + "@visactor/vtable-gantt": "workspace:1.26.5", + "@visactor/react-vtable": "workspace:1.26.5", + "@visactor/vue-vtable": "workspace:1.26.5", + "@visactor/openinula-vtable": "workspace:1.26.5", + "@visactor/vtable-editors": "workspace:1.26.5", + "@visactor/vtable-export": "workspace:1.26.5", + "@visactor/vtable-search": "workspace:1.26.5", + "@visactor/vtable-sheet": "workspace:1.26.5", "buble": "^0.20.0", - "@visactor/vchart": "2.0.13-alpha.10", + "@visactor/vchart": "2.1.3", "markdown-it": "^13.0.0", "highlight.js": "^11.8.0", "axios": "^1.4.0", diff --git a/packages/openinula-vtable/demo/src/list-table/list-custom-layout.tsx b/packages/openinula-vtable/demo/src/list-table/list-custom-layout.tsx index 26d55c53b0..1efc7d47b3 100644 --- a/packages/openinula-vtable/demo/src/list-table/list-custom-layout.tsx +++ b/packages/openinula-vtable/demo/src/list-table/list-custom-layout.tsx @@ -72,11 +72,9 @@ function App() { text: 'item.label', fill: '#000' }} - stateProxy={stateName => { - if (stateName === 'hover') { - return { - fill: 'red' - }; + states={{ + hover: { + fill: 'red' } }} onMouseEnter={event => { diff --git a/packages/openinula-vtable/package.json b/packages/openinula-vtable/package.json index 06b70d8a3f..8ee511ea6a 100644 --- a/packages/openinula-vtable/package.json +++ b/packages/openinula-vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/openinula-vtable", - "version": "1.26.4", + "version": "1.26.5", "description": "The openinula version of VTable", "keywords": [ "openinula", @@ -44,13 +44,13 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.4", + "@visactor/vtable": "workspace:1.26.5", "@visactor/vutils": "~1.0.17" }, "devDependencies": { "cross-env": "^7.0.3", "increase-memory-limit": "^1.0.7", - "@visactor/vchart": "2.0.13-alpha.10", + "@visactor/vchart": "2.1.3", "@internal/bundler": "workspace:*", "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", diff --git a/packages/react-vtable/demo/src/component/custom-layout-dom.tsx b/packages/react-vtable/demo/src/component/custom-layout-dom.tsx index 763296ac61..505ecb797c 100644 --- a/packages/react-vtable/demo/src/component/custom-layout-dom.tsx +++ b/packages/react-vtable/demo/src/component/custom-layout-dom.tsx @@ -94,11 +94,9 @@ const CustomLayoutComponent = (props: CustomLayoutFunctionArg & { text: string } text: `${text}-${row}`, fill: hover ? 'red' : '#000' }} - // stateProxy={(stateName: any) => { - // if (stateName === 'hover') { - // return { - // fill: 'red' - // }; + // states={{ + // hover: { + // fill: 'red' // } // }} onMouseEnter={(event: any) => { diff --git a/packages/react-vtable/demo/src/component/custom-layout.tsx b/packages/react-vtable/demo/src/component/custom-layout.tsx index ffa90ba9f1..f7ca72dae3 100644 --- a/packages/react-vtable/demo/src/component/custom-layout.tsx +++ b/packages/react-vtable/demo/src/component/custom-layout.tsx @@ -244,11 +244,9 @@ const CustomLayoutComponent = (props: CustomLayoutFunctionArg & { text: string } text: `${text}-${row}`, fill: hover ? 'red' : '#000' }} - // stateProxy={(stateName: any) => { - // if (stateName === 'hover') { - // return { - // fill: 'red' - // }; + // states={{ + // hover: { + // fill: 'red' // } // }} onMouseEnter={(event: any) => { diff --git a/packages/react-vtable/demo/src/list-table/list-custom-layout.tsx b/packages/react-vtable/demo/src/list-table/list-custom-layout.tsx index fb7800e61f..2de5447c6a 100644 --- a/packages/react-vtable/demo/src/list-table/list-custom-layout.tsx +++ b/packages/react-vtable/demo/src/list-table/list-custom-layout.tsx @@ -72,11 +72,9 @@ function App() { text: 'item.label', fill: '#000' }} - stateProxy={stateName => { - if (stateName === 'hover') { - return { - fill: 'red' - }; + states={{ + hover: { + fill: 'red' } }} onMouseEnter={event => { diff --git a/packages/react-vtable/package.json b/packages/react-vtable/package.json index ba75a1a5e3..9b5dec4c02 100644 --- a/packages/react-vtable/package.json +++ b/packages/react-vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vtable", - "version": "1.26.4", + "version": "1.26.5", "description": "The react version of VTable", "keywords": [ "react", @@ -57,7 +57,7 @@ "react-dom": "^18.2.0 || ^19.0.0" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.4", + "@visactor/vtable": "workspace:1.26.5", "@visactor/vutils": "~1.0.17", "react-is": "^18.2.0", "react-reconciler": "0.29.0" @@ -65,7 +65,7 @@ "devDependencies": { "cross-env": "^7.0.3", "increase-memory-limit": "^1.0.7", - "@visactor/vchart": "2.0.13-alpha.10", + "@visactor/vchart": "2.1.3", "@internal/bundler": "workspace:*", "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", diff --git a/packages/react-vtable/src/components/vrender-components/type.ts b/packages/react-vtable/src/components/vrender-components/type.ts index a6da676aa4..f901b7a13c 100644 --- a/packages/react-vtable/src/components/vrender-components/type.ts +++ b/packages/react-vtable/src/components/vrender-components/type.ts @@ -1,8 +1,10 @@ import type { Ref } from 'react'; -import type { IEventParamsType } from '@visactor/vtable/es/vrender'; +import type { IEventParamsType, StateDefinitionsInput } from '@visactor/vtable/es/vrender'; -export type GraphicProps = { +export type GraphicProps, IGraphicElement> = { attribute?: IGraphicGraphicAttribute; - ref?: Ref; + ref?: Ref; + states?: StateDefinitionsInput; + sharedStateDefinitions?: StateDefinitionsInput>; } & IGraphicGraphicAttribute & IEventParamsType; diff --git a/packages/react-vtable/src/table-components/custom/graphic.ts b/packages/react-vtable/src/table-components/custom/graphic.ts index 0a99b91586..c1f3032622 100644 --- a/packages/react-vtable/src/table-components/custom/graphic.ts +++ b/packages/react-vtable/src/table-components/custom/graphic.ts @@ -16,13 +16,16 @@ import type { IRectGraphicAttribute, ISymbolGraphicAttribute, IRichTextGraphicAttribute, - IPolygonGraphicAttribute + IPolygonGraphicAttribute, + StateDefinitionsInput } from '@visactor/vtable/es/vrender'; -type GraphicProps = { +type GraphicProps> = { attribute: IGraphicGraphicAttribute; ref?: Ref; children?: ReactNode; + states?: StateDefinitionsInput; + sharedStateDefinitions?: StateDefinitionsInput>; } & IEventParamsType; export const Group: ( diff --git a/packages/react-vtable/src/table-components/custom/vtable-browser-env-contribution.ts b/packages/react-vtable/src/table-components/custom/vtable-browser-env-contribution.ts index e061bcd490..81de4d5b20 100644 --- a/packages/react-vtable/src/table-components/custom/vtable-browser-env-contribution.ts +++ b/packages/react-vtable/src/table-components/custom/vtable-browser-env-contribution.ts @@ -1,6 +1,6 @@ import { getTargetCell } from '@visactor/vtable'; import type { CreateDOMParamsType, IGraphic } from '@visactor/vtable/es/vrender'; -import { ContainerModule, EnvContribution, BrowserEnvContribution } from '@visactor/vtable/es/vrender'; +import { EnvContribution, BrowserEnvContribution } from '@visactor/vtable/es/vrender'; import { isString } from '@visactor/vutils'; export type CreateDOMParamsTypeForVTable = CreateDOMParamsType & { @@ -8,14 +8,14 @@ export type CreateDOMParamsTypeForVTable = CreateDOMParamsType & { style?: Record; }; -export const reactEnvModule = new ContainerModule((bind, unbind, isBound, rebind) => { +export const reactEnvModule = ({ bind, isBound, rebind }: any) => { bind(VTableBrowserEnvContribution).toSelf().inSingletonScope(); if (isBound(EnvContribution)) { rebind(EnvContribution).toService(VTableBrowserEnvContribution); } else { bind(EnvContribution).toService(VTableBrowserEnvContribution); } -}); +}; class VTableBrowserEnvContribution extends BrowserEnvContribution { updateDom(dom: HTMLElement, params: CreateDOMParamsTypeForVTable): boolean { diff --git a/packages/react-vtable/src/tables/base-table.tsx b/packages/react-vtable/src/tables/base-table.tsx index 6677f74d54..8d96949aa2 100644 --- a/packages/react-vtable/src/tables/base-table.tsx +++ b/packages/react-vtable/src/tables/base-table.tsx @@ -67,7 +67,7 @@ export type BaseTableProps = EventsProps & // for react-vtable if (isBrowserEnv()) { - container.load(reactEnvModule); + (container as any).load(reactEnvModule); } type Props = React.PropsWithChildren; diff --git a/packages/vtable-calendar/package.json b/packages/vtable-calendar/package.json index 1d2fd909ab..82426ee528 100644 --- a/packages/vtable-calendar/package.json +++ b/packages/vtable-calendar/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-calendar", - "version": "1.26.4", + "version": "1.26.5", "description": "The calendar component of VTable", "author": { "name": "VisActor", @@ -37,7 +37,7 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.4", + "@visactor/vtable": "workspace:1.26.5", "@visactor/vutils": "~1.0.17", "date-fns": "3.6.0" }, diff --git a/packages/vtable-editors/package.json b/packages/vtable-editors/package.json index d915b63f85..f1311d8053 100644 --- a/packages/vtable-editors/package.json +++ b/packages/vtable-editors/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-editors", - "version": "1.26.4", + "version": "1.26.5", "description": "", "sideEffects": false, "main": "cjs/index.js", diff --git a/packages/vtable-export/package.json b/packages/vtable-export/package.json index d1e692f8c1..07e72d5733 100644 --- a/packages/vtable-export/package.json +++ b/packages/vtable-export/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-export", - "version": "1.26.4", + "version": "1.26.5", "description": "The export util of VTable", "author": { "name": "VisActor", @@ -37,7 +37,7 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.4", + "@visactor/vtable": "workspace:1.26.5", "@visactor/vutils": "~1.0.17", "file-saver": "2.0.5", "@types/file-saver": "2.0.7", @@ -46,7 +46,7 @@ "devDependencies": { "cross-env": "^7.0.3", "increase-memory-limit": "^1.0.7", - "@visactor/vchart": "2.0.13-alpha.10", + "@visactor/vchart": "2.1.3", "@internal/bundler": "workspace:*", "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", diff --git a/packages/vtable-gantt/__tests__/gantt-bugserver-init.test.ts b/packages/vtable-gantt/__tests__/gantt-bugserver-init.test.ts new file mode 100644 index 0000000000..bec18d12d3 --- /dev/null +++ b/packages/vtable-gantt/__tests__/gantt-bugserver-init.test.ts @@ -0,0 +1,462 @@ +// @ts-nocheck + +global.__VERSION__ = 'none'; + +import { createDiv } from './dom'; +import { Gantt } from '../src/index'; +import { defaultPixelRatio } from '../src/tools/pixel-ratio'; + +describe('bugserver gantt initialization', () => { + test('initializes gantt with mixed date formats and top-level columns', () => { + const container = createDiv(); + container.style.width = '800px'; + container.style.height = '400px'; + + const records = [ + { + id: 1, + title: 'Software Development', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-04', + end: '2024-07-14', + progress: 31, + priority: 'P0' + }, + { + id: 2, + title: 'Scope', + developer: 'liufangfang.jane@bytedance.com', + start: '07/14/2024', + end: '07/24/2024', + progress: 60, + priority: 'P0' + }, + { + id: 3, + title: 'Determine project scope', + developer: 'liufangfang.jane@bytedance.com', + start: '2024.07.06', + end: '2024.07.08', + progress: 100, + priority: 'P1' + }, + { + id: 4, + title: 'Release', + developer: 'liufangfang.jane@bytedance.com', + start: '2024/07/24', + end: '2024/08/04', + progress: 90, + priority: 'P0' + } + ]; + + const columns = [ + { field: 'title', title: 'title', width: 200, sort: true }, + { field: 'start', title: 'start', width: 150, sort: true }, + { field: 'end', title: 'end', width: 150, sort: true }, + { field: 'priority', title: 'priority', width: 100, sort: true }, + { field: 'progress', title: 'progress', width: 200, sort: true } + ]; + + const gantt = new Gantt(container, { + records, + columns, + defaultHeaderRowHeight: 60, + defaultRowHeight: 40, + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress', + labelText: '{title} {progress}%', + barStyle: { + width: 20, + barColor: '#ee8800', + completedBarColor: '#91e8e0', + cornerRadius: 8, + borderWidth: 1, + borderColor: 'black' + } + }, + timelineHeader: { + colWidth: 60, + scales: [ + { unit: 'week', step: 1, startOfWeek: 'sunday' }, + { unit: 'day', step: 1 } + ] + }, + minDate: '2024-07-05', + maxDate: '2024-10-15', + markLine: [ + { + date: '2024-07-17', + style: { + lineWidth: 1, + lineColor: 'blue', + lineDash: [8, 4] + } + } + ], + scrollStyle: { + visible: 'scrolling' + }, + overscrollBehavior: 'none' + }); + + expect(gantt.scenegraph.stage).toBeDefined(); + + gantt.release?.(); + container.remove(); + }); + + test('initializes the gantt stage with the configured pixel ratio', () => { + const container = createDiv(); + container.style.width = '1600px'; + container.style.height = '800px'; + + const gantt = new Gantt(container, { + pixelRatio: 3, + records: [ + { id: 101, title: '需求评审', owner: 'Alice', start: '2024-12-05', end: '2024-12-12', progress: 20 }, + { id: 102, title: '交互设计', owner: 'Bob', start: '2024-12-10', end: '2024-12-18', progress: 35 } + ], + taskListTable: { + columns: [ + { field: 'title', title: 'title', width: 160, sort: true }, + { field: 'owner', title: 'owner', width: 80, sort: true }, + { field: 'start', title: 'start', width: 120, sort: true } + ], + tableWidth: 360, + minTableWidth: 280, + maxTableWidth: 640 + }, + taskKeyField: 'id', + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress', + moveable: true, + labelText: '{title}' + }, + minDate: '2024-12-01', + maxDate: '2024-12-31', + timelineHeader: { + colWidth: 30, + scales: [{ unit: 'day', step: 1 }] + }, + scrollStyle: { + visible: 'scrolling' + } + }); + + expect((gantt.scenegraph.stage as any).window.dpr).toBe(gantt.parsedOptions.pixelRatio); + expect(gantt.canvas.width).toBe(parseFloat(gantt.canvas.style.width) * gantt.parsedOptions.pixelRatio); + expect(gantt.canvas.height).toBe(parseFloat(gantt.canvas.style.height) * gantt.parsedOptions.pixelRatio); + + gantt.release?.(); + container.remove(); + }); + + test('uses the browser pixel ratio by default for gantt owned canvas', () => { + const container = createDiv(); + container.style.width = '800px'; + container.style.height = '200px'; + + const gantt = new Gantt(container, { + records: [ + { + id: 1, + title: '项目规划', + developer: '张三', + startDate: '2024-07-05', + endDate: '2024-07-14', + baselineStartDate: '2024-07-01', + baselineEndDate: '2024-07-10', + progress: 80 + }, + { + id: 2, + title: '需求分析', + developer: '李四', + startDate: '2024-07-08', + endDate: '2024-07-12', + baselineStartDate: '2024-07-03', + baselineEndDate: '2024-07-08', + progress: 100 + } + ], + taskListTable: { + columns: [ + { field: 'title', title: '任务名称', width: 80 }, + { field: 'developer', title: '负责人', width: 80 }, + { field: 'progress', title: '进度', width: 80, format: (val: number) => `${val}%` } + ], + tableWidth: 'auto', + minTableWidth: 100, + maxTableWidth: 500 + }, + headerRowHeight: 50, + rowHeight: 90, + taskBar: { + startDateField: 'startDate', + endDateField: 'endDate', + progressField: 'progress', + baselineStartDateField: 'baselineStartDate', + baselineEndDateField: 'baselineEndDate', + labelText: '{title}', + barStyle: { + width: 25, + barColor: '#3498db', + completedBarColor: '#27ae60', + cornerRadius: 5 + }, + baselineStyle: { + width: 15, + barColor: 'gray', + cornerRadius: 5 + } + }, + timelineHeader: { + colWidth: 50, + scales: [ + { unit: 'month', step: 1 }, + { unit: 'week', step: 1, startOfWeek: 'monday' }, + { unit: 'day', step: 1 } + ] + }, + minDate: '2024-06-30', + maxDate: '2024-09-01' + }); + + expect(gantt.parsedOptions.pixelRatio).toBe(defaultPixelRatio); + expect((gantt.scenegraph.stage as any).window.dpr).toBe(defaultPixelRatio); + expect(gantt.canvas.width).toBe(parseFloat(gantt.canvas.style.width) * defaultPixelRatio); + expect(gantt.canvas.height).toBe(parseFloat(gantt.canvas.style.height) * defaultPixelRatio); + + gantt.release?.(); + container.remove(); + }); + + test('keeps gantt owned canvas scaled after task table split resize', () => { + const container = createDiv(); + container.style.width = '800px'; + container.style.height = '400px'; + + const gantt = new Gantt(container, { + pixelRatio: 2, + records: [ + { id: 1, title: 'Software Development', start: '2024-07-15', end: '2024-07-16', progress: 31 }, + { id: 2, title: 'Scope', start: '2024-07-16', end: '2024-07-17', progress: 60 }, + { id: 3, title: 'Determine project scope', start: '2024/07/17', end: '2024/07/18', progress: 100 } + ], + taskListTable: { + columns: [ + { field: 'title', title: 'title', width: 200, sort: true }, + { field: 'start', title: 'start', width: 150, sort: true } + ], + tableWidth: 400, + minTableWidth: 100, + maxTableWidth: 600 + }, + frame: { + verticalSplitLineMoveable: true, + outerFrameStyle: { + borderLineWidth: 2, + borderColor: 'red', + cornerRadius: 8 + } + }, + headerRowHeight: 60, + rowHeight: 40, + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress', + labelText: '{title} {progress}%' + }, + timelineHeader: { + colWidth: 60, + scales: [ + { unit: 'week', step: 1, startOfWeek: 'sunday' }, + { unit: 'day', step: 1 } + ] + }, + minDate: '2024-07-14', + maxDate: '2024-10-15', + rowSeriesNumber: { + title: '行号', + dragOrder: true + } + }); + + const stateManager = gantt.stateManager as any; + stateManager.resizeTableWidth.resizing = true; + stateManager.resizeTableWidth.lastX = 400; + + stateManager.dealResizeTableWidth({ pageX: 350 } as MouseEvent); + + expect(gantt.taskTableWidth).toBe(350); + expect((gantt.scenegraph.stage as any).window.dpr).toBe(gantt.parsedOptions.pixelRatio); + expect(gantt.canvas.width).toBe(parseFloat(gantt.canvas.style.width) * gantt.parsedOptions.pixelRatio); + expect(gantt.canvas.height).toBe(parseFloat(gantt.canvas.style.height) * gantt.parsedOptions.pixelRatio); + + gantt.release?.(); + container.remove(); + }); + + test('shows clipped task label poptip on hover after owned canvas DPR setup', () => { + const container = createDiv(); + container.style.width = '600px'; + container.style.height = '400px'; + + const gantt = new Gantt(container, { + records: [ + { + id: 1, + title: 'Software Development', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-04 08:30:00', + end: '2024-07-04 12:29:59', + progress: 31, + priority: 'P0' + }, + { + id: 2, + title: 'Scope', + developer: 'liufangfang.jane@bytedance.com', + start: '2024-07-04 00:30:00', + end: '2024-07-04 17:59:59', + progress: 60, + priority: 'P0' + } + ], + taskListTable: { + columns: [ + { field: 'title', title: 'title', width: 200, sort: true }, + { field: 'start', title: 'start', width: 150, sort: true }, + { field: 'end', title: 'end', width: 150, sort: true } + ], + tableWidth: 100, + minTableWidth: 100, + maxTableWidth: 600 + }, + headerRowHeight: 40, + rowHeight: 40, + taskBar: { + selectable: false, + startDateField: 'start', + endDateField: 'end', + progressField: 'progress', + labelText: '{title} {progress}%', + labelTextStyle: { + fontFamily: 'Arial', + fontSize: 16, + textAlign: 'left' + }, + barStyle: { + width: 20, + barColor: '#ee8800', + completedBarColor: '#91e8e0', + cornerRadius: 10 + } + }, + timelineHeader: { + colWidth: 30, + scales: [ + { unit: 'year', step: 3, style: { textStick: true } }, + { unit: 'month', step: 1, style: { textStick: true } }, + { unit: 'quarter', step: 1, style: { textStick: true } }, + { unit: 'week', step: 2, startOfWeek: 'sunday', style: { textStick: true } }, + { unit: 'day', step: 2 }, + { unit: 'hour', step: 1 } + ] + }, + minDate: '2024-07-03 18:00:00', + maxDate: '2024-07-25', + rowSeriesNumber: { + title: '行号', + dragOrder: true + }, + scrollStyle: { + visible: 'scrolling' + } + }); + + const taskBarNode = gantt.scenegraph.taskBar.getTaskBarNodeByIndex(0) as any; + const label = taskBarNode.textLabel; + + const poptipPlugin = gantt.scenegraph.stage.pluginService.findPluginsByName('poptipForText')[0] as any; + + expect(label).toBeDefined(); + expect(label.cliped).toBe(true); + expect(poptipPlugin).toBeDefined(); + + poptipPlugin.poptip({ target: label }); + gantt.scenegraph.stage.render(); + + const interactiveLayer = gantt.scenegraph.stage.getLayer('_builtin_interactive') as any; + expect(interactiveLayer?.getChildren().some((child: any) => child.name === 'poptip')).toBe(true); + + gantt.release?.(); + container.remove(); + }); + + test('initializes the data zoom stage with the configured pixel ratio', () => { + const container = createDiv(); + container.style.width = '1600px'; + container.style.height = '800px'; + + const gantt = new Gantt(container, { + pixelRatio: 3, + records: [ + { id: 101, title: '需求评审', owner: 'Alice', start: '2024-12-05', end: '2024-12-12', progress: 20 }, + { id: 102, title: '交互设计', owner: 'Bob', start: '2024-12-10', end: '2024-12-18', progress: 35 } + ], + taskListTable: { + columns: [ + { field: 'title', title: 'title', width: 160, sort: true }, + { field: 'owner', title: 'owner', width: 80, sort: true }, + { field: 'start', title: 'start', width: 120, sort: true } + ], + tableWidth: 360, + minTableWidth: 280, + maxTableWidth: 640 + }, + taskKeyField: 'id', + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress', + moveable: true, + labelText: '{title}' + }, + minDate: '2024-12-01', + maxDate: '2024-12-31', + timelineHeader: { + colWidth: 30, + scales: [{ unit: 'day', step: 1 }], + zoomScale: { + enabled: true, + levels: [] + } + }, + scrollStyle: { + visible: 'scrolling' + } + }); + + const dataZoom = gantt.zoomScaleManager.createDataZoomIntegration({ + width: 400, + height: 30, + x: 360, + y: 0 + }); + + expect((dataZoom as any).stage.window.dpr).toBe(gantt.parsedOptions.pixelRatio); + expect((dataZoom as any).canvas.width).toBe(400 * gantt.parsedOptions.pixelRatio); + expect((dataZoom as any).canvas.height).toBe(30 * gantt.parsedOptions.pixelRatio); + + gantt.release?.(); + container.remove(); + }); +}); diff --git a/packages/vtable-gantt/__tests__/gantt-sort-scales.test.ts b/packages/vtable-gantt/__tests__/gantt-sort-scales.test.ts new file mode 100644 index 0000000000..ee14e82c58 --- /dev/null +++ b/packages/vtable-gantt/__tests__/gantt-sort-scales.test.ts @@ -0,0 +1,86 @@ +// @ts-nocheck + +global.__VERSION__ = 'none'; + +import { Gantt } from '../src/index'; + +describe('Gantt._sortScales', () => { + // Regression test for https://github.com/VisActor/VTable/issues/5159 + // When only `zoomScale` is configured (so `timelineHeader.scales` is still + // undefined), `_sortScales` used to crash on `timelineScales.length`. + test('does not throw when timelineHeader.scales is undefined', () => { + const context = { + options: { timelineHeader: {} }, + parsedOptions: {} + }; + + expect(() => Gantt.prototype._sortScales.call(context)).not.toThrow(); + expect(context.parsedOptions.sortedTimelineScales.map(scale => scale.unit)).toEqual(['day']); + expect(context.parsedOptions.reverseSortedTimelineScales.map(scale => scale.unit)).toEqual(['day']); + }); + + test('does not share mutable default scale objects across calls', () => { + const firstContext = { + options: {}, + parsedOptions: {} + }; + const secondContext = { + options: {}, + parsedOptions: {} + }; + + Gantt.prototype._sortScales.call(firstContext); + firstContext.parsedOptions.sortedTimelineScales[0].timelineDates = ['stale']; + Gantt.prototype._sortScales.call(secondContext); + + expect(secondContext.parsedOptions.sortedTimelineScales[0].timelineDates).toBeUndefined(); + }); + + test('still sorts the configured scales', () => { + const context = { + options: { timelineHeader: { scales: [{ unit: 'day' }, { unit: 'month' }] } }, + parsedOptions: {} + }; + + Gantt.prototype._sortScales.call(context); + + expect(context.parsedOptions.sortedTimelineScales.map(scale => scale.unit)).toEqual(['month', 'day']); + expect(context.parsedOptions.reverseSortedTimelineScales.map(scale => scale.unit)).toEqual(['day', 'month']); + }); + + test('uses zoomScale current level when timelineHeader.scales is undefined', () => { + const context = { + options: { + timelineHeader: { + zoomScale: { + enabled: true, + levels: [ + [ + { unit: 'month', step: 1 }, + { unit: 'day', step: 1 } + ] + ] + } + } + }, + zoomScaleManager: { + config: { + levels: [ + [ + { unit: 'month', step: 1 }, + { unit: 'day', step: 1 } + ] + ] + }, + getCurrentLevel: () => 0 + }, + parsedOptions: {} + }; + + Gantt.prototype._sortScales.call(context); + + expect(context.options.timelineHeader.scales.map(scale => scale.unit)).toEqual(['month', 'day']); + expect(context.parsedOptions.sortedTimelineScales.map(scale => scale.unit)).toEqual(['month', 'day']); + expect(context.parsedOptions.reverseSortedTimelineScales.map(scale => scale.unit)).toEqual(['day', 'month']); + }); +}); diff --git a/packages/vtable-gantt/__tests__/listTable-event-type.test.ts b/packages/vtable-gantt/__tests__/listTable-event-type.test.ts new file mode 100644 index 0000000000..87b81a9687 --- /dev/null +++ b/packages/vtable-gantt/__tests__/listTable-event-type.test.ts @@ -0,0 +1,12 @@ +// @ts-nocheck + +global.__VERSION__ = 'none'; + +import { TABLE_EVENT_TYPE, VTable } from '../src'; + +describe('gantt list table event type export', () => { + test('exports ListTable event types from package entry', () => { + expect(TABLE_EVENT_TYPE).toBe(VTable.TABLE_EVENT_TYPE); + expect(TABLE_EVENT_TYPE.CLICK_CELL).toBe('click_cell'); + }); +}); diff --git a/packages/vtable-gantt/examples/vite.config.js b/packages/vtable-gantt/examples/vite.config.js index 1178d110f1..3511a001bf 100644 --- a/packages/vtable-gantt/examples/vite.config.js +++ b/packages/vtable-gantt/examples/vite.config.js @@ -14,6 +14,7 @@ module.exports = { resolve: { alias: { '@visactor/vtable-editors': path.resolve(__dirname, '../../vtable-editors/src/index.ts'), + '@visactor/vtable/es/vrender-app': path.resolve(__dirname, '../../vtable/src/vrender-app.ts'), '@visactor/vtable/es/vrender': path.resolve(__dirname, '../../vtable/src/vrender.ts'), '@visactor/vtable': path.resolve(__dirname, '../../vtable/src/index.ts'), '@src': path.resolve(__dirname, '../../vtable/src/'), diff --git a/packages/vtable-gantt/jest.config.js b/packages/vtable-gantt/jest.config.js index b85a9b7249..22a51740e3 100644 --- a/packages/vtable-gantt/jest.config.js +++ b/packages/vtable-gantt/jest.config.js @@ -1,5 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-var-requires const path = require('path'); +const { createVRenderModuleNameMapper } = require('../../common/config/jest/vrender-module-name-mapper'); module.exports = { preset: 'ts-jest', @@ -13,10 +14,12 @@ module.exports = { diagnostics: { exclude: ['**'] }, - tsconfig: { + tsconfig: { resolveJsonModule: true, esModuleInterop: true, paths: { + '@visactor/vtable/es/vrender-app': ['../vtable/src/vrender-app.ts'], + '@visactor/vtable/es/vrender': ['../vtable/src/vrender.ts'], '@src/vrender': ['../vtable/src/vrender.ts'], '@src/*': ['../vtable/src/*'] } @@ -52,8 +55,10 @@ module.exports = { 'd3-dsv': path.resolve(__dirname, './node_modules/d3-dsv/dist/d3-dsv.min.js'), 'd3-hexbin': path.resolve(__dirname, './node_modules/d3-hexbin/build/d3-hexbin.min.js'), 'd3-hierarchy': path.resolve(__dirname, './node_modules/d3-hierarchy/dist/d3-hierarchy.min.js'), + ...createVRenderModuleNameMapper('/../vtable/node_modules'), '^@visactor/vtable-editors$': path.resolve(__dirname, '../vtable-editors/src/index.ts'), '^@visactor/vtable/es/themes$': path.resolve(__dirname, '../vtable/src/themes.ts'), + '^@visactor/vtable/es/vrender-app$': path.resolve(__dirname, '../vtable/src/vrender-app.ts'), '^@visactor/vtable/es/vrender$': path.resolve(__dirname, '../vtable/src/vrender.ts'), '^@visactor/vtable$': path.resolve(__dirname, '../vtable/src/index.ts'), '^@src/vrender$': path.resolve(__dirname, '../vtable/src/vrender.ts'), diff --git a/packages/vtable-gantt/package.json b/packages/vtable-gantt/package.json index 5da89165a5..973a50d806 100644 --- a/packages/vtable-gantt/package.json +++ b/packages/vtable-gantt/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-gantt", - "version": "1.26.4", + "version": "1.26.5", "description": "canvas table width high performance", "keywords": [ "vtable-gantt", @@ -50,8 +50,8 @@ "fix-memory-limit": "cross-env LIMIT=10240 increase-memory-limit" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.4", - "@visactor/vtable-editors": "workspace:1.26.4", + "@visactor/vtable": "workspace:1.26.5", + "@visactor/vtable-editors": "workspace:1.26.5", "@visactor/vutils": "~1.0.17", "@visactor/vscale": "~1.0.17", "@visactor/vdataset": "~1.0.17", diff --git a/packages/vtable-gantt/src/Gantt.ts b/packages/vtable-gantt/src/Gantt.ts index 4084a9713e..bf77edc652 100644 --- a/packages/vtable-gantt/src/Gantt.ts +++ b/packages/vtable-gantt/src/Gantt.ts @@ -84,6 +84,9 @@ export function createRootElement(padding: any, className: string = 'vtable-gant return element; } + +const DEFAULT_TIMELINE_SCALE: ITimelineScale = { unit: 'day', step: 1 }; + export class Gantt extends EventTarget { options: GanttConstructorOptions; container: HTMLElement; @@ -689,45 +692,58 @@ export class Gantt extends EventTarget { _sortScales() { const { timelineHeader } = this.options; - if (timelineHeader) { - const timelineScales = timelineHeader.scales; - const sortOrder = ['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second']; - if (timelineScales.length === 1) { - if ( - timelineScales[0].unit === 'hour' || - timelineScales[0].unit === 'minute' || - timelineScales[0].unit === 'second' - ) { - this.parsedOptions.timeScaleIncludeHour = true; - } - } - const orderedScales = timelineScales.slice().sort((a, b) => { - if (a.unit === 'hour' || a.unit === 'minute' || a.unit === 'second') { - this.parsedOptions.timeScaleIncludeHour = true; - } - const indexA = sortOrder.indexOf(a.unit); - const indexB = sortOrder.indexOf(b.unit); - if (indexA === -1) { - return 1; - } else if (indexB === -1) { - return -1; - } - return indexA - indexB; - }); - const reverseOrderedScales = timelineScales.slice().sort((a, b) => { - const indexA = sortOrder.indexOf(a.unit); - const indexB = sortOrder.indexOf(b.unit); - if (indexA === -1) { - return 1; - } else if (indexB === -1) { - return -1; - } - return indexB - indexA; - }); + const zoomLevelScales = this.zoomScaleManager?.config.levels[this.zoomScaleManager.getCurrentLevel()]; + const defaultTimelineScale: ITimelineScale = { ...DEFAULT_TIMELINE_SCALE }; + let timelineScales: ITimelineScale[]; + if (timelineHeader?.scales?.length > 0) { + timelineScales = timelineHeader.scales; + } else if (zoomLevelScales?.length > 0) { + timelineScales = zoomLevelScales; + } else { + timelineScales = [defaultTimelineScale]; + } + + if (timelineHeader && (!timelineHeader.scales || timelineHeader.scales.length === 0)) { + timelineHeader.scales = timelineScales.map(scale => ({ ...scale })); + } - this.parsedOptions.sortedTimelineScales = orderedScales; - this.parsedOptions.reverseSortedTimelineScales = reverseOrderedScales; + const sortOrder = ['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second']; + this.parsedOptions.timeScaleIncludeHour = false; + if (timelineScales.length === 1) { + if ( + timelineScales[0].unit === 'hour' || + timelineScales[0].unit === 'minute' || + timelineScales[0].unit === 'second' + ) { + this.parsedOptions.timeScaleIncludeHour = true; + } } + const orderedScales = timelineScales.slice().sort((a, b) => { + if (a.unit === 'hour' || a.unit === 'minute' || a.unit === 'second') { + this.parsedOptions.timeScaleIncludeHour = true; + } + const indexA = sortOrder.indexOf(a.unit); + const indexB = sortOrder.indexOf(b.unit); + if (indexA === -1) { + return 1; + } else if (indexB === -1) { + return -1; + } + return indexA - indexB; + }); + const reverseOrderedScales = timelineScales.slice().sort((a, b) => { + const indexA = sortOrder.indexOf(a.unit); + const indexB = sortOrder.indexOf(b.unit); + if (indexA === -1) { + return 1; + } else if (indexB === -1) { + return -1; + } + return indexB - indexA; + }); + + this.parsedOptions.sortedTimelineScales = orderedScales; + this.parsedOptions.reverseSortedTimelineScales = reverseOrderedScales; } _generateTimeLineDateMap() { diff --git a/packages/vtable-gantt/src/gantt-helper.ts b/packages/vtable-gantt/src/gantt-helper.ts index 47f09f0561..854ce69322 100644 --- a/packages/vtable-gantt/src/gantt-helper.ts +++ b/packages/vtable-gantt/src/gantt-helper.ts @@ -1,5 +1,6 @@ import type { Group } from '@visactor/vtable/es/vrender'; import type { Gantt } from './Gantt'; +import { defaultPixelRatio } from './tools/pixel-ratio'; import { TasksShowMode, type IMarkLine, @@ -126,7 +127,7 @@ export { isNode }; export function initOptions(gantt: Gantt) { const options = gantt.options; gantt.parsedOptions.tasksShowMode = options?.tasksShowMode ?? TasksShowMode.Tasks_Separate; - gantt.parsedOptions.pixelRatio = options?.pixelRatio ?? 1; + gantt.parsedOptions.pixelRatio = options?.pixelRatio ?? defaultPixelRatio; gantt.parsedOptions.rowHeight = options?.rowHeight ?? 40; gantt.parsedOptions.timelineColWidth = options?.timelineHeader?.colWidth ?? 60; gantt.parsedOptions.startDateField = options.taskBar?.startDateField ?? 'startDate'; diff --git a/packages/vtable-gantt/src/index.ts b/packages/vtable-gantt/src/index.ts index f6d24c3415..5bb262eef3 100644 --- a/packages/vtable-gantt/src/index.ts +++ b/packages/vtable-gantt/src/index.ts @@ -13,6 +13,7 @@ import type { TextAlignType, TextBaselineType } from '@visactor/vtable'; +import { TABLE_EVENT_TYPE } from '@visactor/vtable'; import { Gantt } from './Gantt'; import * as tools from './tools'; import * as VRender from './vrender'; @@ -37,6 +38,7 @@ export { GroupColumnDefine, TextAlignType, TextBaselineType, + TABLE_EVENT_TYPE, tools, VRender, VTable, diff --git a/packages/vtable-gantt/src/scenegraph/graphic/group-contribution-render.ts b/packages/vtable-gantt/src/scenegraph/graphic/group-contribution-render.ts index 109e35df8e..edd3ac87ed 100644 --- a/packages/vtable-gantt/src/scenegraph/graphic/group-contribution-render.ts +++ b/packages/vtable-gantt/src/scenegraph/graphic/group-contribution-render.ts @@ -8,7 +8,12 @@ import type { IGroup, IGroupRenderContribution } from '@visactor/vtable/es/vrender'; -import { injectable, BaseRenderContributionTime } from '@visactor/vtable/es/vrender'; +import { BaseRenderContributionTime } from '@visactor/vtable/es/vrender'; + +const injectable = + () => + (target: T): T => + target; // const highlightDash: number[] = []; diff --git a/packages/vtable-gantt/src/scenegraph/graphic/index.ts b/packages/vtable-gantt/src/scenegraph/graphic/index.ts index 1d6b6c9e3d..1df8328bb2 100644 --- a/packages/vtable-gantt/src/scenegraph/graphic/index.ts +++ b/packages/vtable-gantt/src/scenegraph/graphic/index.ts @@ -1,8 +1,8 @@ -import { ContainerModule, GroupRenderContribution } from '@visactor/vtable/es/vrender'; +import { GroupRenderContribution } from '@visactor/vtable/es/vrender'; import { DateHeaderGroupBeforeRenderContribution } from './group-contribution-render'; -export default new ContainerModule((bind, unbind, isBound, rebind) => { +export default ({ bind }: any) => { // text 渲染器注入contributions // bind(TextStickBeforeRenderContribution).toSelf().inSingletonScope(); // bind(TextRenderContribution).toService(TextStickBeforeRenderContribution); @@ -10,4 +10,4 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // group 渲染器注入contributions bind(DateHeaderGroupBeforeRenderContribution).toSelf().inSingletonScope(); bind(GroupRenderContribution).toService(DateHeaderGroupBeforeRenderContribution); -}); +}; diff --git a/packages/vtable-gantt/src/scenegraph/scenegraph.ts b/packages/vtable-gantt/src/scenegraph/scenegraph.ts index ec6e05f6cd..d68465655d 100644 --- a/packages/vtable-gantt/src/scenegraph/scenegraph.ts +++ b/packages/vtable-gantt/src/scenegraph/scenegraph.ts @@ -1,5 +1,6 @@ import type { Stage } from '@visactor/vtable/es/vrender'; -import { container, Group, vglobal, createStage } from '@visactor/vtable/es/vrender'; +import { container, Group } from '@visactor/vtable/es/vrender'; +import { createStageFromVRenderApp } from '@visactor/vtable/es/vrender-app'; import { Grid } from './grid'; import type { Gantt } from '../Gantt'; import { Env } from '../env'; @@ -17,7 +18,7 @@ import { DependencyLink, updateLinkLinePoints } from './dependency-link'; import { DragOrderLine } from './drag-order-line'; import type { GanttTaskBarNode } from './gantt-node'; import { TasksShowMode, TaskType } from '../ts-types'; -container.load(graphicContribution); +(container as any).load(graphicContribution); export class Scenegraph { dateStepWidth: number; rowHeight: number; @@ -35,6 +36,7 @@ export class Scenegraph { taskCreationButton: TaskCreationButton; toolTip: ToolTip; stage: Stage; + releaseVRenderAppRef?: () => void; tableGroupWidth: number; tableGroupHeight: number; constructor(gantt: Gantt) { @@ -48,27 +50,32 @@ export class Scenegraph { // width = table.canvasWidth; // height = table.canvasHeight; } else { - vglobal.setEnv('browser'); - width = gantt.canvas.width; - height = gantt.canvas.height; + width = gantt.canvas.parentElement?.offsetWidth ?? gantt.canvas.width; + height = gantt.canvas.parentElement?.offsetHeight ?? gantt.canvas.height; } - this.stage = createStage({ - canvas: gantt.canvas, - width, - height, - disableDirtyBounds: false, - background: gantt.parsedOptions.underlayBackgroundColor, - // dpr: gantt.internalProps.pixelRatio, - enableLayout: true, - autoRender: false, - context: { - appName: 'vtable' + const { stage, releaseAppRef } = createStageFromVRenderApp( + { + canvas: gantt.canvas, + width, + height, + disableDirtyBounds: false, + background: gantt.parsedOptions.underlayBackgroundColor, + dpr: gantt.parsedOptions.pixelRatio, + enableLayout: true, + autoRender: false, + canvasControled: true, + context: { + appName: 'vtable' + }, + pluginList: ['poptipForText'] + // afterRender: () => { + // this._gantt.fireListeners('after_render', null); + // } }, - pluginList: ['poptipForText'] - // afterRender: () => { - // this._gantt.fireListeners('after_render', null); - // } - }); + { mode: Env.mode === 'node' ? 'node' : 'browser', scope: 'vtable-gantt' } + ); + this.stage = stage as Stage; + this.releaseVRenderAppRef = releaseAppRef; (this.stage as any).gantt = this._gantt; (this.stage as any).table = this._gantt; // 为了使用bindDebugTool this.stage.defaultLayer.setTheme({ @@ -130,9 +137,8 @@ export class Scenegraph { let height; if (Env.mode === 'node') { } else { - vglobal.setEnv('browser'); - width = gantt.canvas.width; - height = gantt.canvas.height; + width = gantt.canvas.parentElement?.offsetWidth ?? gantt.canvas.width / gantt.parsedOptions.pixelRatio; + height = gantt.canvas.parentElement?.offsetHeight ?? gantt.canvas.height / gantt.parsedOptions.pixelRatio; } this.stage.resize(width, height); this.refreshAll(); @@ -282,7 +288,14 @@ export class Scenegraph { this.updateNextFrame(); } release() { - this.stage.release(); + const releaseAppRef = this.releaseVRenderAppRef; + this.releaseVRenderAppRef = undefined; + + try { + this.stage.release(); + } finally { + releaseAppRef?.(); + } } showTaskCreationButton(x: number, y: number, dateIndex: number) { diff --git a/packages/vtable-gantt/src/tools/pixel-ratio.ts b/packages/vtable-gantt/src/tools/pixel-ratio.ts index f1581b9da5..09cb1efd67 100644 --- a/packages/vtable-gantt/src/tools/pixel-ratio.ts +++ b/packages/vtable-gantt/src/tools/pixel-ratio.ts @@ -1,4 +1,4 @@ -import { isNode } from '../gantt-helper'; +const isNode = typeof window === 'undefined' || typeof window.window === 'undefined'; export let defaultPixelRatio = 1; /* diff --git a/packages/vtable-gantt/src/vrender-vtable-compat.d.ts b/packages/vtable-gantt/src/vrender-vtable-compat.d.ts new file mode 100644 index 0000000000..b6b1f6c542 --- /dev/null +++ b/packages/vtable-gantt/src/vrender-vtable-compat.d.ts @@ -0,0 +1,25 @@ +declare module '@visactor/vtable/es/vrender-app' { + type VRenderAppEnv = 'browser' | 'node' | 'worker' | 'wx' | 'lynx' | 'harmony' | 'taro' | 'feishu' | 'tt'; + type VRenderStageMode = VRenderAppEnv | 'desktop-browser'; + + export type VRenderStageAppOptions = { + mode?: VRenderStageMode; + scope?: string; + app?: import('@visactor/vrender-core').IApp; + stage?: import('@visactor/vrender-core').IStage; + envParams?: import('@visactor/vrender-core').IEnvParamsMap[VRenderAppEnv]; + }; + + export type VRenderStageAppRef = { + app?: import('@visactor/vrender-core').IApp; + stage: import('@visactor/vrender-core').IStage; + releaseAppRef: () => void; + stageOwned: boolean; + appOwned: boolean; + }; + + export function createStageFromVRenderApp( + params: Partial, + options?: VRenderStageAppOptions + ): VRenderStageAppRef; +} diff --git a/packages/vtable-gantt/src/zoom-scale/DataZoomIntegration.ts b/packages/vtable-gantt/src/zoom-scale/DataZoomIntegration.ts index b28805eff9..921ab6d000 100644 --- a/packages/vtable-gantt/src/zoom-scale/DataZoomIntegration.ts +++ b/packages/vtable-gantt/src/zoom-scale/DataZoomIntegration.ts @@ -1,5 +1,6 @@ import type { Gantt } from '../Gantt'; -import { DataZoom, createStage, vglobal } from '@visactor/vtable/es/vrender'; +import { DataZoom, vglobal } from '@visactor/vtable/es/vrender'; +import { createStageFromVRenderApp } from '@visactor/vtable/es/vrender-app'; export interface DataZoomConfig { /** DataZoom 容器 ID(可选,如果不提供会自动从 Gantt 实例获取) */ @@ -35,6 +36,7 @@ export class DataZoomIntegration { private gantt: Gantt; private dataZoomAxis: DataZoom; private stage: any; + private releaseVRenderAppRef?: () => void; private canvas: HTMLCanvasElement; private isUpdatingFromDataZoom = false; private isUpdatingFromGantt = false; @@ -179,12 +181,19 @@ export class DataZoomIntegration { dataZoomWrapper.appendChild(this.canvas); ganttContainer.appendChild(dataZoomWrapper); - this.stage = createStage({ - canvas: this.canvas, - width, - height, - autoRender: true - }); + const { stage, releaseAppRef } = createStageFromVRenderApp( + { + canvas: this.canvas, + width, + height, + dpr: this.gantt.parsedOptions.pixelRatio, + canvasControled: true, + autoRender: true + }, + { mode: 'browser', scope: 'vtable-gantt-datazoom' } + ); + this.stage = stage; + this.releaseVRenderAppRef = releaseAppRef; // 创建 DataZoom 实例 this.dataZoomAxis = new DataZoom({ @@ -620,7 +629,14 @@ export class DataZoomIntegration { } if (this.stage) { - this.stage.release(); + const releaseAppRef = this.releaseVRenderAppRef; + this.releaseVRenderAppRef = undefined; + + try { + this.stage.release(); + } finally { + releaseAppRef?.(); + } } } } diff --git a/packages/vtable-gantt/tsconfig.eslint.json b/packages/vtable-gantt/tsconfig.eslint.json index ec39ccb9f1..9f7035cdde 100644 --- a/packages/vtable-gantt/tsconfig.eslint.json +++ b/packages/vtable-gantt/tsconfig.eslint.json @@ -12,6 +12,9 @@ "@visactor/vtable": [ "../vtable/src" ], + "@visactor/vtable/es/vrender-app": [ + "../vtable/src/vrender-app.ts" + ], "@visactor/vtable/es/vrender": [ "../vtable/src/vrender.ts" ] diff --git a/packages/vtable-gantt/tsconfig.json b/packages/vtable-gantt/tsconfig.json index 21a3c4de00..e376438f3f 100644 --- a/packages/vtable-gantt/tsconfig.json +++ b/packages/vtable-gantt/tsconfig.json @@ -32,4 +32,4 @@ "module": "commonjs" } } -} \ No newline at end of file +} diff --git a/packages/vtable-plugins/jest.config.js b/packages/vtable-plugins/jest.config.js index ae54607661..07b4dcce41 100644 --- a/packages/vtable-plugins/jest.config.js +++ b/packages/vtable-plugins/jest.config.js @@ -2,6 +2,7 @@ const path = require('path'); const isCI = process.env.CI === 'true' || process.env.CI === '1'; +const { createVRenderModuleNameMapper } = require('../../common/config/jest/vrender-module-name-mapper'); module.exports = { preset: 'ts-jest', @@ -62,9 +63,12 @@ module.exports = { __dirname, '../../common/temp/node_modules/.pnpm/d3-hierarchy@3.1.2/node_modules/d3-hierarchy/dist/d3-hierarchy.min.js' ), + ...createVRenderModuleNameMapper('/../vtable/node_modules'), + '^@visactor/vtable-editors$': path.resolve(__dirname, '../vtable-editors/src/index.ts'), + '^@visactor/vtable/es/ts-types$': '/../vtable/src/ts-types/index.ts', '^@visactor/vtable/es/(.*)$': '/../vtable/src/$1', - '@visactor/vtable': path.resolve(__dirname, '../vtable/src/index.ts'), - '@src/vrender': path.resolve(__dirname, '../vtable/src/vrender.ts') + '^@visactor/vtable$': path.resolve(__dirname, '../vtable/src/index.ts'), + '^@src/vrender$': path.resolve(__dirname, '../vtable/src/vrender.ts') }, setupFiles: ['./setup-mock.js'] }; diff --git a/packages/vtable-plugins/package.json b/packages/vtable-plugins/package.json index 4e49a9db8d..7ba93aab23 100644 --- a/packages/vtable-plugins/package.json +++ b/packages/vtable-plugins/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-plugins", - "version": "1.26.4", + "version": "1.26.5", "description": "The search util of VTable", "author": { "name": "VisActor", @@ -61,10 +61,10 @@ "devDependencies": { "cross-env": "^7.0.3", "increase-memory-limit": "^1.0.7", - "@visactor/vtable": "workspace:1.26.4", - "@visactor/vtable-editors": "workspace:1.26.4", - "@visactor/vtable-gantt": "workspace:1.26.4", - "@visactor/vchart": "2.0.13-alpha.10", + "@visactor/vtable": "workspace:1.26.5", + "@visactor/vtable-editors": "workspace:1.26.5", + "@visactor/vtable-gantt": "workspace:1.26.5", + "@visactor/vchart": "2.1.3", "@internal/bundler": "workspace:*", "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", diff --git a/packages/vtable-plugins/src/table-series-number.ts b/packages/vtable-plugins/src/table-series-number.ts index 8919a1cded..ea87ff781a 100644 --- a/packages/vtable-plugins/src/table-series-number.ts +++ b/packages/vtable-plugins/src/table-series-number.ts @@ -3,6 +3,8 @@ import { TABLE_EVENT_TYPE, TYPES } from '@visactor/vtable'; import { TableSeriesNumber as VRenderTableSeriesNumber, SeriesNumberEvent } from '@visactor/vtable/es/vrender'; import type { ILayer, TableSeriesNumberAttributes } from '@visactor/vtable/es/vrender'; +const FEDERATED_EVENT_API_ERROR = 'DisplayObject cannot propagate events outside of the Federated Events API'; + export type TableSeriesNumberOptions = { rowCount: number; colCount: number; @@ -136,9 +138,66 @@ export class TableSeriesNumber implements pluginsDefinition.IVTablePlugin { return this.pluginOptions.dragOrder?.enableDragRowOrder ? this.table.isRowSelected(rowIndex) : false; } }); + this.patchSeriesNumberEventDispatch(); this.listenComponentEvents(); } + private patchSeriesNumberEventDispatch() { + const component = this.seriesNumberComponent as any; + const originalDispatchEvent = component._dispatchEvent?.bind(component); + if (!originalDispatchEvent) { + return; + } + + component._dispatchEvent = (eventName: string, detail?: any) => { + try { + return originalDispatchEvent(eventName, detail); + } catch (err) { + if (!(err instanceof Error) || !err.message.includes(FEDERATED_EVENT_API_ERROR)) { + throw err; + } + this.dispatchSeriesNumberEventToLocalListeners(eventName, detail); + } + }; + } + + private dispatchSeriesNumberEventToLocalListeners(eventName: string, detail?: any) { + const component = this.seriesNumberComponent as any; + const listeners = component?._events?.[eventName]; + if (!listeners) { + return; + } + + const event = { + type: eventName, + detail, + target: component, + currentTarget: component, + defaultPrevented: false, + propagationStopped: false, + propagationImmediatelyStopped: false, + preventDefault() { + this.defaultPrevented = true; + }, + stopPropagation() { + this.propagationStopped = true; + }, + stopImmediatePropagation() { + this.propagationImmediatelyStopped = true; + } + }; + const listenerList = Array.isArray(listeners) ? listeners.slice() : [listeners]; + listenerList.forEach(listener => { + if (typeof listener === 'function') { + listener(event); + } else if (typeof listener?.fn === 'function') { + listener.fn.call(listener.context, event); + } else if (typeof listener?.handleEvent === 'function') { + listener.handleEvent(event); + } + }); + } + run(...args: any[]) { // const eventArgs = args[0]; const runTime = args[1]; diff --git a/packages/vtable-search/package.json b/packages/vtable-search/package.json index 0002f0190d..9e7e52e62f 100644 --- a/packages/vtable-search/package.json +++ b/packages/vtable-search/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-search", - "version": "1.26.4", + "version": "1.26.5", "description": "The search util of VTable", "author": { "name": "VisActor", @@ -36,13 +36,13 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.4", + "@visactor/vtable": "workspace:1.26.5", "@visactor/vutils": "~1.0.17" }, "devDependencies": { "cross-env": "^7.0.3", "increase-memory-limit": "^1.0.7", - "@visactor/vchart": "2.0.13-alpha.10", + "@visactor/vchart": "2.1.3", "@internal/bundler": "workspace:*", "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", diff --git a/packages/vtable-sheet/__tests__/basic-formula-test.test.ts b/packages/vtable-sheet/__tests__/basic-formula-test.test.ts index 48541809b5..d790f25c58 100644 --- a/packages/vtable-sheet/__tests__/basic-formula-test.test.ts +++ b/packages/vtable-sheet/__tests__/basic-formula-test.test.ts @@ -1,4 +1,5 @@ import { FormulaManager } from '../src/managers/formula-manager'; +import { FormulaEngine } from '../src/formula/formula-engine'; // Mock VTableSheet for testing const mockVTableSheet = { @@ -92,4 +93,36 @@ describe('Basic Formula Functionality', () => { expect(formulas['A1']).toBe('=SUM(A1:A2)'); expect(formulas['A2']).toBe('=A1*2'); }); + + test('should evaluate arithmetic formulas without executing cell content as code', () => { + const engine = new FormulaEngine(); + const marker = '__vtableSheetRceExecuted'; + const payload = `0,globalThis.${marker}=1,0`; + + delete (globalThis as any)[marker]; + engine.addSheet('Sheet1', [['', '']]); + engine.setActiveSheet('Sheet1'); + + engine.updateSheetData('Sheet1', [[payload], ['=A1+1']]); + const batchResult = engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 0 }); + expect(batchResult.error).toBeTruthy(); + expect((globalThis as any)[marker]).toBeUndefined(); + + engine.setCellContent({ sheet: 'Sheet1', row: 0, col: 0 }, payload); + engine.setCellContent({ sheet: 'Sheet1', row: 0, col: 1 }, '=A1+1'); + const singleResult = engine.getCellValue({ sheet: 'Sheet1', row: 0, col: 1 }); + expect(singleResult.error).toBeTruthy(); + expect((globalThis as any)[marker]).toBeUndefined(); + }); + + test('should keep numeric arithmetic, precedence and functions working', () => { + const engine = new FormulaEngine(); + engine.addSheet('Sheet1', [[2], ['=A1+1'], ['=SUM(A1:A2)+3'], ['=1+2*3'], ['=-(1+2)*3']]); + engine.setActiveSheet('Sheet1'); + + expect(engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 0 })).toEqual({ value: 3, error: undefined }); + expect(engine.getCellValue({ sheet: 'Sheet1', row: 2, col: 0 })).toEqual({ value: 8, error: undefined }); + expect(engine.getCellValue({ sheet: 'Sheet1', row: 3, col: 0 })).toEqual({ value: 7, error: undefined }); + expect(engine.getCellValue({ sheet: 'Sheet1', row: 4, col: 0 })).toEqual({ value: -9, error: undefined }); + }); }); diff --git a/packages/vtable-sheet/__tests__/multi-header-record-data.test.ts b/packages/vtable-sheet/__tests__/multi-header-record-data.test.ts index 755dacaeee..23ceb87e57 100644 --- a/packages/vtable-sheet/__tests__/multi-header-record-data.test.ts +++ b/packages/vtable-sheet/__tests__/multi-header-record-data.test.ts @@ -61,3 +61,49 @@ test('VTableSheet keeps record fields when top-level columns mix leaf and groupe removeDom(container); } }); + +test('VTableSheet normalizes object record data with nested field paths for formulas', () => { + const container = createDiv() as HTMLDivElement; + container.style.position = 'relative'; + container.style.width = '1000px'; + container.style.height = '800px'; + + const sheet = new VTableSheet(container, { + showFormulaBar: false, + showSheetTab: false, + defaultRowHeight: 25, + defaultColWidth: 100, + sheets: [ + { + sheetKey: 'nestedFieldSheet', + sheetTitle: '嵌套字段示例', + active: true, + columns: [ + { title: '名称', field: 'name', width: 120 }, + { title: '营业额', field: ['sales', 'amount'], width: 120 } + ], + data: [ + { + name: '商品A', + sales: { + amount: '125' + } + } + ] + } + ] + }); + + try { + const value = sheet.formulaManager.formulaEngine.getCellValue({ + sheet: 'nestedFieldSheet', + row: 1, + col: 1 + }); + + expect(value).toEqual({ value: 125, error: undefined }); + } finally { + sheet.release(); + removeDom(container); + } +}); diff --git a/packages/vtable-sheet/examples/menu.ts b/packages/vtable-sheet/examples/menu.ts index dfaba92593..2265304622 100644 --- a/packages/vtable-sheet/examples/menu.ts +++ b/packages/vtable-sheet/examples/menu.ts @@ -26,5 +26,9 @@ export const menus = [ { path: 'sheet', name: 'history' + }, + { + path: 'sheet', + name: 'issue-5204-formula-manager' } ]; diff --git a/packages/vtable-sheet/examples/sheet/issue-5204-formula-manager.ts b/packages/vtable-sheet/examples/sheet/issue-5204-formula-manager.ts new file mode 100644 index 0000000000..57a3b0afce --- /dev/null +++ b/packages/vtable-sheet/examples/sheet/issue-5204-formula-manager.ts @@ -0,0 +1,66 @@ +import { VTableSheet } from '../../src/index'; +import type { IColumnDefine, SheetData } from '../../src/ts-types'; + +const CONTAINER_ID = 'vTable'; + +const columns: IColumnDefine[] = [ + { title: '测试公式计算', field: 'test', width: 120 }, + { + title: 'B商品营业额', + field: ['sales', 'Bmoney'], + width: 120 + }, + { + title: 'A商品营业额', + field: 'Amoney', + width: 120 + } +]; + +const dataA: SheetData = Array.from({ length: 12 }, (_, index) => ({ + test: `${index}-row`, + sales: { + Bmoney: `${(index + 1) * 50}` + }, + Amoney: `${(index + 2) * 50}` +})); + +const columnsB = [ + { title: '测试公式计算', width: 120 }, + { + title: 'B商品营业额', + width: 120 + }, + { + title: 'A商品营业额', + width: 120 + } +]; + +const dataB = [['1', 2, 3]]; + +export function createTable() { + const container = document.getElementById(CONTAINER_ID)!; + window.sheetInstance = new VTableSheet(container, { + showFormulaBar: false, + showSheetTab: true, + defaultRowHeight: 32, + defaultColWidth: 120, + sheets: [ + { + sheetKey: 'Issue-5204-a', + sheetTitle: 'data是Array', + columns, + data: dataA, + active: true + }, + { + sheetKey: 'Issue-5204-b', + sheetTitle: 'data是Array>', + columns: columnsB, + data: dataB, + active: true + } + ] + }); +} diff --git a/packages/vtable-sheet/jest.config.js b/packages/vtable-sheet/jest.config.js index e5dd6e8d49..01dfebe76c 100644 --- a/packages/vtable-sheet/jest.config.js +++ b/packages/vtable-sheet/jest.config.js @@ -1,5 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-var-requires const path = require('path'); +const { createVRenderModuleNameMapper } = require('../../common/config/jest/vrender-module-name-mapper'); module.exports = { preset: 'ts-jest', @@ -66,6 +67,7 @@ module.exports = { __dirname, '../../common/temp/node_modules/.pnpm/d3-hierarchy@3.1.2/node_modules/d3-hierarchy/dist/d3-hierarchy.min.js' ), + ...createVRenderModuleNameMapper('/../vtable/node_modules'), '@visactor/vtable-editors': path.resolve(__dirname, '../vtable-editors/src/index.ts'), '@visactor/vtable-plugins': path.resolve(__dirname, '../vtable-plugins/src/index.ts'), // 子路径映射需放在 @visactor/vtable 之前,确保精确匹配优先生效 diff --git a/packages/vtable-sheet/package.json b/packages/vtable-sheet/package.json index 29e6fc9b97..18708a86c9 100644 --- a/packages/vtable-sheet/package.json +++ b/packages/vtable-sheet/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-sheet", - "version": "1.26.4", + "version": "1.26.5", "description": "Lightweight editable spreadsheet component based on VTable", "keywords": [ "vtable-sheet", @@ -50,9 +50,9 @@ "build:es": "bundle --clean -f es --ignorePostTasks" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.4", - "@visactor/vtable-editors": "workspace:1.26.4", - "@visactor/vtable-plugins": "workspace:1.26.4", + "@visactor/vtable": "workspace:1.26.5", + "@visactor/vtable-editors": "workspace:1.26.5", + "@visactor/vtable-plugins": "workspace:1.26.5", "@visactor/vutils": "~1.0.17", "@visactor/vscale": "~1.0.17", "@visactor/vdataset": "~1.0.17", diff --git a/packages/vtable-sheet/src/components/vtable-sheet.ts b/packages/vtable-sheet/src/components/vtable-sheet.ts index 1b312a085f..4122db8b26 100644 --- a/packages/vtable-sheet/src/components/vtable-sheet.ts +++ b/packages/vtable-sheet/src/components/vtable-sheet.ts @@ -10,7 +10,8 @@ import type { IVTableSheetUpdateOptions, ISheetDefine, IWorkSheetOptions, - IColumnDefine + IColumnDefine, + SheetData } from '../ts-types'; import type { MultiSheetImportResult } from '@visactor/vtable-plugins/src/excel-import/types'; import type { TableEventHandlersEventArgumentMap } from '@visactor/vtable/es/ts-types/events'; @@ -1356,7 +1357,7 @@ export default class VTableSheet { * @param sheetKey sheet的key * @returns 数据 */ - exportData(sheetKey: string): any[][] { + exportData(sheetKey: string): SheetData { const sheet = this.workSheetInstances.get(sheetKey); if (!sheet) { return []; @@ -1368,7 +1369,7 @@ export default class VTableSheet { * 导出所有sheet的数据 * @returns 数据 */ - exportAllData(): any[][] { + exportAllData(): SheetData[] { const sheets = Array.from(this.workSheetInstances.values()); return sheets.map(sheet => sheet.getData()); } diff --git a/packages/vtable-sheet/src/core/WorkSheet.ts b/packages/vtable-sheet/src/core/WorkSheet.ts index a1b0f45fff..0473123dea 100644 --- a/packages/vtable-sheet/src/core/WorkSheet.ts +++ b/packages/vtable-sheet/src/core/WorkSheet.ts @@ -11,7 +11,7 @@ import type { IThemeDefine, IFilterConfig, IFilterState, - ISheetDefine, + SheetData, IVTableSheetUpdateOptions } from '../ts-types'; import type { TYPES, VTableSheet } from '..'; @@ -27,7 +27,7 @@ import type { IWorksheetEventSource } from '../event/event-interfaces'; */ export type WorkSheetConstructorOptions = { /** 表格数据 */ - data?: any[][]; + data?: SheetData; /** 公式计算选项 */ formula?: IFormulaManagerOptions; /** Sheet 唯一标识 */ @@ -782,7 +782,7 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { /** * 获取表格数据 */ - getData(): any[][] { + getData(): SheetData { // 从表格实例获取数据 return this.options.data || []; } @@ -806,9 +806,9 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { } } - const data = this.getData(); - if (data && data[row] && data[row][col] !== undefined) { - return data[row][col]; + const rowData = this.getData()[row]; + if (Array.isArray(rowData) && rowData[col] !== undefined) { + return rowData[col]; } return null; } @@ -848,9 +848,9 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { * @param value 新值 */ setCellValue(col: number, row: number, value: any): void { - const data = this.getData(); - if (data && data[row]) { - data[row][col] = value; + const rowData = this.getData()[row]; + if (Array.isArray(rowData)) { + rowData[col] = value; // 更新表格实例 if (this.tableInstance) { diff --git a/packages/vtable-sheet/src/formula/formula-engine.ts b/packages/vtable-sheet/src/formula/formula-engine.ts index 7633ba4a51..52e17f9b97 100644 --- a/packages/vtable-sheet/src/formula/formula-engine.ts +++ b/packages/vtable-sheet/src/formula/formula-engine.ts @@ -1502,26 +1502,147 @@ export class FormulaEngine { if (funcResult.error) { return { value: null, error: `Error in function ${funcCall}: ${funcResult.error}` }; } + const numericFuncValue = this.toArithmeticNumber(funcResult.value); + if (numericFuncValue.error) { + return { value: null, error: numericFuncValue.error }; + } processedExpr = - processedExpr.slice(0, funcSpan.start) + String(funcResult.value) + processedExpr.slice(funcSpan.end + 1); + processedExpr.slice(0, funcSpan.start) + + String(numericFuncValue.value) + + processedExpr.slice(funcSpan.end + 1); } // 3. 处理剩余的单元格引用(包括带sheet前缀的引用,支持带引号的sheet名称) const cellRefs = processedExpr.match(/('[^']+'!)?([A-Za-z0-9_\s一-龥]+!)?[A-Z]+[0-9]+/g) || []; for (const cellRef of cellRefs) { const value = this.getCellValueByA1(cellRef); - processedExpr = processedExpr.replace(cellRef, String(value)); + const numericValue = this.toArithmeticNumber(value); + if (numericValue.error) { + return { value: null, error: numericValue.error }; + } + processedExpr = processedExpr.replace(cellRef, String(numericValue.value)); } - // 4. 计算最终的算术表达式 - // eslint-disable-next-line @typescript-eslint/no-implied-eval - const result = Function('"use strict"; return (' + processedExpr + ')')(); - return { value: result, error: undefined }; + // 4. 使用白名单算术解析器计算,禁止通过 Function/eval 执行用户可控表达式 + return this.evaluateBasicArithmetic(processedExpr); } catch (error) { return { value: null, error: 'Basic arithmetic evaluation failed' }; } } + private toArithmeticNumber(value: unknown): { value: number; error?: string } { + if (value === null || value === undefined || value === '') { + return { value: 0, error: undefined }; + } + + const num = Number(value); + if (!Number.isFinite(num)) { + return { value: 0, error: 'Arithmetic operands must be numeric' }; + } + + return { value: num, error: undefined }; + } + + private evaluateBasicArithmetic(expr: string): { value: unknown; error?: string } { + try { + let index = 0; + + const skipWhitespace = () => { + while (index < expr.length && /\s/.test(expr[index])) { + index++; + } + }; + + const parseNumber = (): number | null => { + skipWhitespace(); + const match = expr.slice(index).match(/^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?/i); + if (!match) { + return null; + } + index += match[0].length; + return Number(match[0]); + }; + + const parseFactor = (): number => { + skipWhitespace(); + + if (expr[index] === '+') { + index++; + return parseFactor(); + } + if (expr[index] === '-') { + index++; + return -parseFactor(); + } + if (expr[index] === '(') { + index++; + const value = parseAdditive(); + skipWhitespace(); + if (expr[index] !== ')') { + throw new Error('Missing closing parenthesis'); + } + index++; + return value; + } + + const value = parseNumber(); + if (value === null || !Number.isFinite(value)) { + throw new Error('Invalid arithmetic token'); + } + return value; + }; + + const parseMultiplicative = (): number => { + let value = parseFactor(); + while (true) { + skipWhitespace(); + const operator = expr[index]; + if (operator !== '*' && operator !== '/') { + break; + } + index++; + const right = parseFactor(); + if (operator === '*') { + value *= right; + } else { + value /= right; + } + } + return value; + }; + + const parseAdditive = (): number => { + let value = parseMultiplicative(); + while (true) { + skipWhitespace(); + const operator = expr[index]; + if (operator !== '+' && operator !== '-') { + break; + } + index++; + const right = parseMultiplicative(); + if (operator === '+') { + value += right; + } else { + value -= right; + } + } + return value; + }; + + const result = parseAdditive(); + skipWhitespace(); + + if (index !== expr.length || !Number.isFinite(result)) { + return { value: null, error: 'Basic arithmetic evaluation failed' }; + } + + return { value: result, error: undefined }; + } catch { + return { value: null, error: 'Basic arithmetic evaluation failed' }; + } + } + private findInnermostFunctionCallSpan(expr: string): { start: number; end: number } | null { type Frame = { funcStart: number | null }; const stack: Frame[] = []; diff --git a/packages/vtable-sheet/src/managers/formula-manager.ts b/packages/vtable-sheet/src/managers/formula-manager.ts index 3846876290..d87fe9e4b0 100644 --- a/packages/vtable-sheet/src/managers/formula-manager.ts +++ b/packages/vtable-sheet/src/managers/formula-manager.ts @@ -2,7 +2,7 @@ import { FormulaEngine } from '../formula/formula-engine'; import type VTableSheet from '../components/vtable-sheet'; import type { FormulaCell, FormulaResult, IFormulaManager } from '../ts-types/formula'; import { FormulaRangeSelector } from '../formula/formula-range-selector'; -import type { CellRange, ISheetDefine } from '../ts-types'; +import type { CellRange, ISheetDefine, SheetData } from '../ts-types'; import { CellHighlightManager } from '../formula'; import type * as VTable from '@visactor/vtable'; import { CrossSheetFormulaHandler } from '../formula/cross-sheet-formula-handler'; @@ -137,19 +137,39 @@ export class FormulaManager implements IFormulaManager { * @param data 工作表数据 * @returns 标准化后的工作表数据 */ - normalizeSheetData(data: unknown[][], tableInstance: VTable.ListTable): unknown[][] { + normalizeSheetData(data: SheetData, tableInstance: VTable.ListTable): unknown[][] { try { //将columns中的title追加到data中 const headerRows: unknown[][] = []; + const headerKeyColMap = new Map(); for (let i = 0; i < tableInstance.columnHeaderLevelCount; i++) { const headerRow: unknown[] = []; for (let j = 0; j < tableInstance.colCount; j++) { const cellValue = tableInstance.getCellValue(j, i); + const filedDefine = tableInstance.getHeaderDefine(j, i); + if (cellValue === filedDefine.title) { + headerKeyColMap.set(j, filedDefine.field); + } headerRow.push(cellValue); } headerRows.push(headerRow); } - const dataCopy = JSON.parse(JSON.stringify(data)); + const colCount = tableInstance.colCount; + let dataCopy: unknown[][]; + const firstRow = data[0]; + if (firstRow && typeof firstRow === 'object' && !Array.isArray(firstRow)) { + dataCopy = data.map((_, rowIndex) => { + const bodyRow = tableInstance.columnHeaderLevelCount + rowIndex; + const rowTemp = new Array(colCount); + for (let col = 0; col < colCount; col++) { + const field = tableInstance.getBodyField(col, bodyRow) ?? headerKeyColMap.get(col); + rowTemp[col] = field !== undefined ? tableInstance.getRawFieldData(field, col, bodyRow) ?? null : null; + } + return rowTemp; + }); + } else { + dataCopy = JSON.parse(JSON.stringify(data)); + } const toNormalizeData = tableInstance.columnHeaderLevelCount > 0 ? [...headerRows].concat(dataCopy) : dataCopy; @@ -1372,7 +1392,6 @@ export class FormulaManager implements IFormulaManager { // 如果还没有对应的 WorkSheet 实例,跳过,后续按需再补充 return; } - const normalizedData = this.normalizeSheetData(worksheetInstance.getData(), worksheetInstance.tableInstance); this.addSheet(sheetKey, normalizedData, sheetDefine.sheetTitle); }); diff --git a/packages/vtable-sheet/src/ts-types/index.ts b/packages/vtable-sheet/src/ts-types/index.ts index 2ee87297f2..ef04dc4076 100644 --- a/packages/vtable-sheet/src/ts-types/index.ts +++ b/packages/vtable-sheet/src/ts-types/index.ts @@ -14,11 +14,13 @@ export interface IFilterConfig { /** 扩展的列定义,添加筛选相关配置;field 可选,构建 ListTable 时由 WorkSheet 按列索引填充 */ export interface IColumnDefine extends Omit { /** 列字段,可选;未指定时由 WorkSheet 按列索引填充 */ - field?: string | number; + field?: ColumnDefine['field']; /** 是否启用筛选功能 */ filter?: boolean; } +export type SheetData = (CellValue[] | null)[] | Record[]; + /** Sheet定义 */ export interface ISheetDefine { /** 标题 */ @@ -32,7 +34,7 @@ export interface ISheetDefine { /** 表头定义 */ columns?: IColumnDefine[]; /** 数据 */ - data?: (CellValue[] | null)[]; + data?: SheetData; /** 是否是当前活动sheet TODO 是不是放到外层更好*/ active?: boolean; cellMerge?: VTableTypes.CustomMergeCellArray; diff --git a/packages/vtable-sheet/src/ts-types/sheet.ts b/packages/vtable-sheet/src/ts-types/sheet.ts index 9fef2af7e1..d20da91437 100644 --- a/packages/vtable-sheet/src/ts-types/sheet.ts +++ b/packages/vtable-sheet/src/ts-types/sheet.ts @@ -1,4 +1,4 @@ -import type { IColumnDefine, IFilterConfig, IFilterState, ISheetDefine } from './index'; +import type { IColumnDefine, IFilterConfig, IFilterState, ISheetDefine, SheetData } from './index'; import type { CellCoord, CellRange, CellValue } from './base'; import type { ListTableConstructorOptions } from '@visactor/vtable'; @@ -13,7 +13,7 @@ export interface IWorkSheetOptions extends Omit CellRange | null; /** 获取数据 */ - getData: () => CellValue[][]; + getData: () => SheetData; /** 获取行数 */ getRowCount: () => number; diff --git a/packages/vtable/CHANGELOG.json b/packages/vtable/CHANGELOG.json index 9ab4e7dde2..de275ffbc7 100644 --- a/packages/vtable/CHANGELOG.json +++ b/packages/vtable/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@visactor/vtable", "entries": [ + { + "version": "1.26.5", + "tag": "@visactor/vtable_v1.26.5", + "date": "Wed, 15 Jul 2026 10:41:26 GMT", + "comments": { + "none": [ + { + "comment": "Render group title for single-column list tables." + }, + { + "comment": "fix: handle null table container" + } + ] + } + }, { "version": "1.26.4", "tag": "@visactor/vtable_v1.26.4", diff --git a/packages/vtable/CHANGELOG.md b/packages/vtable/CHANGELOG.md index 6efebe57c4..21d1c62770 100644 --- a/packages/vtable/CHANGELOG.md +++ b/packages/vtable/CHANGELOG.md @@ -1,6 +1,14 @@ # Change Log - @visactor/vtable -This log was last generated on Mon, 06 Jul 2026 09:35:12 GMT and should not be manually modified. +This log was last generated on Wed, 15 Jul 2026 10:41:26 GMT and should not be manually modified. + +## 1.26.5 +Wed, 15 Jul 2026 10:41:26 GMT + +### Updates + +- Render group title for single-column list tables. +- fix: handle null table container ## 1.26.4 Mon, 06 Jul 2026 09:35:12 GMT diff --git a/packages/vtable/__tests__/animation-appear.test.ts b/packages/vtable/__tests__/animation-appear.test.ts new file mode 100644 index 0000000000..4c800ecc5d --- /dev/null +++ b/packages/vtable/__tests__/animation-appear.test.ts @@ -0,0 +1,142 @@ +// @ts-nocheck + +import { dealWithAnimationAppear } from '../src/scenegraph/animation/appear'; +import { createText, defaultTimeline, registerAnimate } from '../src/vrender'; + +type MockAnimationAppear = Partial<{ + duration: number; + delay: number; + type: 'all' | 'one-by-one'; + direction: 'row' | 'column'; +}>; + +interface MockChild { + attribute: Record; + setAttribute: jest.Mock; + animate: jest.Mock; +} + +function createAnimation() { + const animation = { + wait: jest.fn(() => animation), + to: jest.fn(() => animation), + from: jest.fn(() => animation) + }; + + return animation; +} + +function createChild(opacity?: number) { + const animation = createAnimation(); + const child = { + attribute: opacity === undefined ? {} : { opacity }, + setAttribute: jest.fn((key: string, value: unknown) => { + child.attribute[key] = value; + }), + animate: jest.fn(() => animation) + }; + + return { child, animation }; +} + +function createTable(children: MockChild[], animationAppear: MockAnimationAppear = {}) { + const cellGroup = { + role: 'cell', + forEachChildren: (callback: (child: MockChild) => void) => { + children.forEach(callback); + } + }; + + return { + options: { + animationAppear: { + duration: 1000, + delay: 500, + type: 'one-by-one', + direction: 'row', + ...animationAppear + } + }, + scenegraph: { + proxy: { + colEnd: 0, + rowEnd: 2 + }, + highPerformanceGetCell: jest.fn((_col: number, row: number) => (row === 2 ? cellGroup : null)) + } + }; +} + +describe('dealWithAnimationAppear', () => { + beforeEach(() => { + registerAnimate(); + defaultTimeline.clear(); + }); + + afterEach(() => { + defaultTimeline.clear(); + }); + + it('keeps the final opacity as static state and animates from hidden state', () => { + const first = createChild(); + const second = createChild(0.4); + const table = createTable([first.child, second.child]); + + dealWithAnimationAppear(table as Parameters[0]); + + expect(first.child.attribute.opacity).toBe(1); + expect(second.child.attribute.opacity).toBe(0.4); + + expect(first.animation.wait).toHaveBeenCalledWith(1000); + expect(first.animation.from).toHaveBeenCalledWith({ opacity: 0 }, 1000, 'linear'); + expect(first.animation.to).not.toHaveBeenCalled(); + + expect(second.animation.wait).toHaveBeenCalledWith(1000); + expect(second.animation.from).toHaveBeenCalledWith({ opacity: 0 }, 1000, 'linear'); + expect(second.animation.to).not.toHaveBeenCalled(); + }); + + it('uses the column index for one-by-one column animation delays', () => { + const { child, animation } = createChild(); + const table = createTable([child], { direction: 'column' }); + table.scenegraph.proxy.colEnd = 3; + table.scenegraph.proxy.rowEnd = 0; + table.scenegraph.highPerformanceGetCell.mockImplementation((col: number) => + col === 3 + ? { + role: 'cell', + forEachChildren: (callback: (child: MockChild) => void) => callback(child) + } + : null + ); + + dealWithAnimationAppear(table as Parameters[0]); + + expect(animation.wait).toHaveBeenCalledWith(1500); + expect(animation.from).toHaveBeenCalledWith({ opacity: 0 }, 1000, 'linear'); + }); + + it('keeps VRender static and final opacity visible through the fade appear lifecycle', () => { + const child = createText({ text: 'cell', opacity: 1 }); + child.setFinalAttributes({ opacity: 0 }); + const table = createTable([child], { delay: 0, type: 'all' }); + + dealWithAnimationAppear(table as Parameters[0]); + + expect(child.attribute.opacity).toBe(0); + expect(child.baseAttributes.opacity).toBe(1); + expect(child.getFinalAttribute().opacity).toBe(1); + + defaultTimeline.tick(500); + + expect(child.attribute.opacity).toBeCloseTo(0.5); + expect(child.baseAttributes.opacity).toBe(1); + expect(child.getFinalAttribute().opacity).toBe(1); + + defaultTimeline.tick(500); + + expect(child.attribute.opacity).toBe(1); + expect(child.baseAttributes.opacity).toBe(1); + expect(child.getFinalAttribute().opacity).toBe(1); + }); +}); diff --git a/packages/vtable/__tests__/chart-graphic.test.ts b/packages/vtable/__tests__/chart-graphic.test.ts new file mode 100644 index 0000000000..0e0d859437 --- /dev/null +++ b/packages/vtable/__tests__/chart-graphic.test.ts @@ -0,0 +1,65 @@ +import { Chart } from '../src/scenegraph/graphic/chart'; + +class MockChart { + static globalConfig = { uniqueTooltip: false }; + + spec: unknown; + option: unknown; + + constructor(spec: unknown, option: unknown) { + this.spec = spec; + this.option = option; + } + + renderSync() { + // noop + } + + getStage() { + return { + enableDirtyBounds() { + // noop + } + }; + } +} + +describe('Chart graphic', () => { + test('keeps runtime refs when VRender builds static state snapshots', () => { + const canvas = document.createElement('canvas') as HTMLCanvasElement & { __vtable__?: unknown }; + const tableRef: { internalProps?: unknown } = {}; + tableRef.internalProps = { element: { __vtable__: tableRef } }; + canvas.__vtable__ = tableRef; + + const chart = new Chart(false, { + stroke: false, + x: 0, + y: 0, + width: 100, + height: 80, + canvas, + mode: 'desktop-browser', + modeParams: {}, + spec: { type: 'bar' }, + ClassType: MockChart, + chartInstance: undefined, + dataId: 'data', + data: [], + cellPadding: [0, 0, 0, 0], + dpr: 1, + axes: [], + tableChartOption: {}, + detectPickChartItem: false + } as any); + + expect(chart.attribute.canvas).toBe(canvas); + expect((chart as any).baseAttributes.canvas).toBe(canvas); + + expect(() => (chart as any).buildStaticAttributeSnapshot()).not.toThrow(); + + chart.setAttributes({ width: 120, height: 90 }); + expect(chart.attribute.canvas).toBe(canvas); + expect(chart.attribute.chartInstance).toBe(chart.chartInstance); + expect(chart.attribute.chartInstance).toBe((chart as any).baseAttributes.chartInstance); + }); +}); diff --git a/packages/vtable/__tests__/layout/listTable-group-single-column.test.ts b/packages/vtable/__tests__/layout/listTable-group-single-column.test.ts new file mode 100644 index 0000000000..e540c9be9a --- /dev/null +++ b/packages/vtable/__tests__/layout/listTable-group-single-column.test.ts @@ -0,0 +1,49 @@ +// @ts-nocheck +import { ListTable } from '../../src'; +import { createDiv } from '../dom'; + +global.__VERSION__ = 'none'; + +function collectText(graphic: any): string[] { + const result: string[] = []; + const children = graphic?.children ?? []; + + for (const child of children) { + if (child.type === 'text') { + result.push(child.attribute?.text); + } + result.push(...collectText(child)); + } + + return result; +} + +describe('listTable group single column', () => { + test('renders parent group title when there is only one data column', () => { + const containerDom: HTMLElement = createDiv(); + containerDom.style.position = 'relative'; + containerDom.style.width = '500px'; + containerDom.style.height = '360px'; + + const table = new ListTable(containerDom, { + records: [ + { category: 'Furniture', subCategory: 'Bookcases', value: 'Bookcase' }, + { category: 'Furniture', subCategory: 'Chairs', value: 'Chair' } + ], + columns: [{ field: 'value', title: 'Value', width: 180 }], + widthMode: 'standard', + groupConfig: { + groupBy: ['category', 'subCategory'], + titleFieldFormat: record => `${record.vtableMergeName}(${record.children.length})` + } + }); + + const groupRecord = table.getCellRawRecord(0, 1); + expect(groupRecord.vtableMerge).toBe(true); + + const text = collectText(table.scenegraph.getCell(0, 1, true)); + expect(text).toContain('Furniture(2)'); + + table.release(); + }); +}); diff --git a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts index 14a5b62fc8..4a243da287 100644 --- a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts @@ -135,11 +135,11 @@ describe('listTable init test', () => { expect(frozenTable.getBodyVisibleRowRange()).toEqual({ rowStart: 10, - rowEnd: 29 + rowEnd: 28 }); expect(frozenTable.getBodyVisibleCellRange()).toMatchObject({ rowStart: 10, - rowEnd: 29 + rowEnd: 28 }); }); diff --git a/packages/vtable/__tests__/options/listTable-null-container.test.ts b/packages/vtable/__tests__/options/listTable-null-container.test.ts new file mode 100644 index 0000000000..c65163c3d1 --- /dev/null +++ b/packages/vtable/__tests__/options/listTable-null-container.test.ts @@ -0,0 +1,10 @@ +// @ts-nocheck +import { ListTable } from '../../src'; + +global.__VERSION__ = 'none'; + +describe('ListTable null container', () => { + test('throws a clear error when the container argument is null', () => { + expect(() => new ListTable(null, { columns: [] })).toThrow("vtable's container is undefined"); + }); +}); diff --git a/packages/vtable/__tests__/scenegraph-custom-state.test.ts b/packages/vtable/__tests__/scenegraph-custom-state.test.ts new file mode 100644 index 0000000000..7b96e41d5d --- /dev/null +++ b/packages/vtable/__tests__/scenegraph-custom-state.test.ts @@ -0,0 +1,73 @@ +import { decodeReactDom } from '../src/scenegraph/component/custom'; + +(globalThis as { __VERSION__?: string }).__VERSION__ = 'none'; + +const reactElementType = Symbol.for('react.element'); + +type TestGraphic = { + type: string; + attribute: unknown; + on: jest.Mock; + states?: unknown; + sharedStateDefinitions?: unknown; +}; + +function createElement(type: (params: { attribute: unknown }) => TestGraphic, props: Record) { + return { + $$typeof: reactElementType, + type, + props + }; +} + +function createGraphic(type: string) { + return ({ attribute }: { attribute: unknown }) => ({ + type, + attribute, + on: jest.fn() + }); +} + +describe('decodeReactDom state config', () => { + it('passes local states to graphics', () => { + const states = { + hover: { + fill: 'red' + } + }; + + const graphic = decodeReactDom( + createElement(createGraphic('text'), { + attribute: { + id: 'label', + name: 'label' + }, + states + }) + ); + + expect(graphic.states).toBe(states); + }); + + it('passes shared state definitions to group graphics', () => { + const sharedStateDefinitions = { + hover: { + patch: { + fill: 'red' + } + } + }; + + const group = decodeReactDom( + createElement(createGraphic('group'), { + attribute: { + id: 'owner', + name: 'owner' + }, + sharedStateDefinitions + }) + ); + + expect(group.sharedStateDefinitions).toBe(sharedStateDefinitions); + }); +}); diff --git a/packages/vtable/__tests__/scenegraph-stage-ownership.test.ts b/packages/vtable/__tests__/scenegraph-stage-ownership.test.ts new file mode 100644 index 0000000000..e8bf116fc5 --- /dev/null +++ b/packages/vtable/__tests__/scenegraph-stage-ownership.test.ts @@ -0,0 +1,30 @@ +// @ts-nocheck +import { Scenegraph } from '../src/scenegraph/scenegraph'; + +(globalThis as { __VERSION__?: string }).__VERSION__ = 'none'; + +describe('Scenegraph stage ownership', () => { + test('does not release a borrowed stage or app reference', () => { + const releaseStage = jest.fn(); + const releaseAppRef = jest.fn(); + const removeChild = jest.fn(); + const releaseTableGroup = jest.fn(); + const tableGroup = { + parent: { removeChild }, + release: releaseTableGroup + }; + const scenegraph = Object.create(Scenegraph.prototype); + + scenegraph.stage = { release: releaseStage }; + scenegraph.stageOwned = false; + scenegraph.releaseVRenderAppRef = releaseAppRef; + scenegraph.tableGroup = tableGroup; + + scenegraph.releaseStage(); + + expect(releaseStage).not.toHaveBeenCalled(); + expect(releaseAppRef).toHaveBeenCalledTimes(1); + expect(removeChild).toHaveBeenCalledWith(tableGroup); + expect(releaseTableGroup).toHaveBeenCalledWith(true); + }); +}); diff --git a/packages/vtable/__tests__/scenegraph/group.test.ts b/packages/vtable/__tests__/scenegraph/group.test.ts new file mode 100644 index 0000000000..d855f56498 --- /dev/null +++ b/packages/vtable/__tests__/scenegraph/group.test.ts @@ -0,0 +1,29 @@ +import { Group } from '../../src/scenegraph/graphic/group'; + +describe('scenegraph Group', () => { + test('addCellGroup clones ancestor cell groups before insertion', () => { + const columnGroup = new Group({}); + const existingCell = new Group({}); + existingCell.role = 'cell'; + existingCell.row = 0; + columnGroup.addCellGroup(existingCell); + + const ancestorCell = new Group({ width: 120, height: 24 }); + ancestorCell.role = 'cell'; + ancestorCell.col = 2; + ancestorCell.row = 1; + (columnGroup as any).parent = ancestorCell; + + const insertedCell = columnGroup.addCellGroup(ancestorCell); + + expect(insertedCell).not.toBe(ancestorCell); + expect(insertedCell.parent).toBe(columnGroup); + expect(insertedCell.role).toBe('cell'); + expect(insertedCell.col).toBe(2); + expect(insertedCell.row).toBe(1); + expect(columnGroup.lastChild).toBe(insertedCell); + + columnGroup.removeAllChild(); + (columnGroup as any).parent = null; + }); +}); diff --git a/packages/vtable/__tests__/scenegraph/text-icon-layout.test.ts b/packages/vtable/__tests__/scenegraph/text-icon-layout.test.ts new file mode 100644 index 0000000000..a5de0e9bc0 --- /dev/null +++ b/packages/vtable/__tests__/scenegraph/text-icon-layout.test.ts @@ -0,0 +1,42 @@ +import { Text } from '../../src/vrender'; +import { Group } from '../../src/scenegraph/graphic/group'; +import { dealWithIcon, insertTextBeforeCellIcons } from '../../src/scenegraph/utils/text-icon-layout'; + +describe('text icon layout insertion', () => { + test('clones ancestor text before inserting it into a cell group', () => { + const ancestorText = new Text({ text: 'value' }); + ancestorText.name = 'text'; + (ancestorText as any).addChildUpdateBoundTag = jest.fn(); + const cellGroup = new Group({}); + const iconGroup = new Group({}); + iconGroup.name = 'icon'; + + cellGroup.appendChild(iconGroup); + (cellGroup as any).parent = ancestorText; + + const insertedText = insertTextBeforeCellIcons(cellGroup, ancestorText as any); + + expect(insertedText).not.toBe(ancestorText); + expect(insertedText.name).toBe('text'); + expect(insertedText.parent).toBe(cellGroup); + expect(cellGroup.firstChild).toBe(insertedText); + expect(insertedText.nextSibling).toBe(iconGroup); + + cellGroup.removeAllChild(); + (cellGroup as any).parent = null; + }); + + test('keeps circle image icon shape for image render contribution', () => { + const iconMark = dealWithIcon({ + type: 'image', + src: 'avatar', + name: 'avatar_pic', + shape: 'circle', + width: 30, + height: 20 + } as any); + + expect(iconMark.attribute.shape).toBe('circle'); + expect(iconMark.attribute.cornerRadius).toBeUndefined(); + }); +}); diff --git a/packages/vtable/__tests__/vchart-vrender-app.test.ts b/packages/vtable/__tests__/vchart-vrender-app.test.ts new file mode 100644 index 0000000000..0b809fba96 --- /dev/null +++ b/packages/vtable/__tests__/vchart-vrender-app.test.ts @@ -0,0 +1,92 @@ +// @ts-nocheck + +import VChart from '@visactor/vchart'; +import * as stageApp from '@visactor/vchart/cjs/compile/stage-app.js'; + +describe('VChart VRender app integration', () => { + test('uses an app-scoped VRender stage helper', () => { + const stage = { release: jest.fn() }; + const app = { + createStage: jest.fn(() => stage) + }; + + expect(typeof stageApp.resolveVRenderApp).toBe('function'); + expect(typeof stageApp.createStageFromApp).toBe('function'); + expect(stageApp.resolveVRenderApp(app).app).toBe(app); + expect(stageApp.createStageFromApp(app, { width: 100, height: 80 })).toBe(stage); + expect(app.createStage).toHaveBeenCalledWith({ width: 100, height: 80 }); + }); + + test('renders chart marks through the resolved VRender app', () => { + const canvas = document.createElement('canvas'); + canvas.width = 200; + canvas.height = 120; + + const chart = new VChart( + { + type: 'line', + width: 200, + height: 120, + padding: 10, + background: '#000', + data: { + id: 'data', + values: [ + { x: 'A', y: 20 }, + { x: 'B', y: 90 }, + { x: 'C', y: 45 } + ] + }, + xField: 'x', + yField: 'y', + axes: [ + { orient: 'bottom', visible: false }, + { orient: 'left', visible: false } + ], + line: { + style: { + stroke: '#207BFE', + lineWidth: 3 + } + }, + point: { + style: { + fill: '#207BFE', + stroke: '#207BFE', + size: 8 + } + } + }, + { + renderCanvas: canvas, + mode: 'desktop-browser', + canvasControled: false, + viewBox: { x1: 0, y1: 0, x2: 200, y2: 120 }, + dpr: 1, + interactive: false, + animation: false, + autoFit: false + } + ); + + chart.renderSync(); + + const context = canvas.getContext('2d'); + expect(context).toBeTruthy(); + + const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data; + let bluePixels = 0; + for (let i = 0; i < pixels.length; i += 4) { + const red = pixels[i]; + const green = pixels[i + 1]; + const blue = pixels[i + 2]; + const alpha = pixels[i + 3]; + if (alpha > 0 && red < 80 && green > 80 && blue > 160) { + bluePixels += 1; + } + } + + chart.release(); + expect(bluePixels).toBeGreaterThan(20); + }); +}); diff --git a/packages/vtable/__tests__/vrender-app-lifecycle.test.ts b/packages/vtable/__tests__/vrender-app-lifecycle.test.ts new file mode 100644 index 0000000000..5a04a6bc19 --- /dev/null +++ b/packages/vtable/__tests__/vrender-app-lifecycle.test.ts @@ -0,0 +1,88 @@ +import { ListTable } from '../src'; +import { createStageFromVRenderApp } from '../src/vrender-app'; +import { createCanvas, createDiv, removeDom } from './dom'; + +(globalThis as { __VERSION__?: string }).__VERSION__ = 'none'; + +const columns = [{ field: 'name', caption: 'Name', width: 120 }]; +const records = [{ name: 'Alice' }]; + +describe('VTable VRender app-scoped lifecycle smoke', () => { + test('creates, renders, releases, and recreates a browser-managed stage', () => { + const container = createDiv(); + container.style.position = 'relative'; + container.style.width = '240px'; + container.style.height = '120px'; + const stages: unknown[] = []; + + try { + for (let i = 0; i < 2; i++) { + const beforeRender = jest.fn(); + const table = new ListTable({ + container, + columns, + records, + widthMode: 'standard', + heightMode: 'standard', + defaultRowHeight: 30, + vRenderAppScope: 'unit-browser-lifecycle', + beforeRender + }); + const stage = table.scenegraph.stage as { render: () => void; release: () => void }; + const releaseStage = jest.spyOn(stage, 'release'); + + expect(table.scenegraph.stageOwned).toBe(true); + expect(stage).toBeTruthy(); + expect(stages).not.toContain(stage); + + stage.render(); + expect(beforeRender).toHaveBeenCalled(); + + stages.push(stage); + table.release(); + expect(releaseStage).toHaveBeenCalledTimes(1); + releaseStage.mockRestore(); + } + } finally { + removeDom(container); + } + }); + + test('does not release an externally supplied borrowed stage', () => { + const canvas = createCanvas(); + canvas.width = 240; + canvas.height = 120; + const external = createStageFromVRenderApp( + { + canvas, + width: 240, + height: 120 + }, + { mode: 'browser', scope: 'unit-borrowed-stage' } + ); + const releaseStage = jest.spyOn(external.stage, 'release'); + + try { + const table = new ListTable({ + canvas, + stage: external.stage, + columns, + records, + widthMode: 'standard', + heightMode: 'standard', + defaultRowHeight: 30 + }); + + expect(table.scenegraph.stage).toBe(external.stage); + expect(table.scenegraph.stageOwned).toBe(false); + + table.release(); + expect(releaseStage).not.toHaveBeenCalled(); + } finally { + releaseStage.mockRestore(); + external.stage.release(); + external.releaseAppRef(); + removeDom(canvas); + } + }); +}); diff --git a/packages/vtable/__tests__/vrender-app-renderer.test.ts b/packages/vtable/__tests__/vrender-app-renderer.test.ts new file mode 100644 index 0000000000..5ba544e608 --- /dev/null +++ b/packages/vtable/__tests__/vrender-app-renderer.test.ts @@ -0,0 +1,313 @@ +import '../src/scenegraph/scenegraph'; +import { application, container, TextMeasureContribution, vglobal } from '../src/vrender'; +import { createStageFromVRenderApp } from '../src/vrender-app'; + +(globalThis as { __VERSION__?: string }).__VERSION__ = 'none'; + +type RendererWithGroupContributions = { + constructor?: { name?: string }; + reInit?: () => void; + _groupRenderContribitions?: { constructor?: { name?: string } }[]; +}; + +type RendererWithRenderContributions = { + constructor?: { name?: string }; + reInit?: () => void; + type?: string; + _renderContribitions?: { constructor?: { name?: string } }[]; +}; + +type RendererWithDrawableContributions = { + constructor?: { name?: string }; + reInit?: () => void; + type?: string; + _renderContribitions?: { constructor?: { name?: string }; drawShape?: (...args: unknown[]) => unknown }[]; +}; + +type StageWithDrawContribution = { + renderService?: { + drawContribution?: { + defaultRenderMap?: Map; + }; + }; +}; + +type PickerWithContains = { + constructor?: { name?: string }; + type?: string; + contains?: ( + graphic: { attribute?: Record; AABBBounds: { containsPoint: () => boolean } }, + point: { + x: number; + y: number; + } + ) => unknown; +}; + +function getSelectedRectRenderer(stage: unknown): RendererWithDrawableContributions | undefined { + const renderMap = (stage as StageWithDrawContribution).renderService?.drawContribution?.defaultRenderMap; + + return [...(renderMap?.values?.() ?? [])].find( + renderer => renderer.type === 'rect' || renderer.constructor?.name === 'DefaultCanvasRectRender' + ); +} + +function getSplitRectContributionSources(renderer?: RendererWithDrawableContributions): string[] { + return (renderer?._renderContribitions ?? []) + .filter(contribution => contribution.constructor?.name?.includes('SplitRect')) + .map(contribution => String(contribution.drawShape)); +} + +describe('VRender app renderer installation', () => { + test('uses the VTable split rect contribution in the selected stage renderer', () => { + const canvas = document.createElement('canvas'); + const { stage, releaseAppRef } = createStageFromVRenderApp( + { + canvas, + width: 100, + height: 100 + }, + { mode: 'browser', scope: 'unit-selected-rect-renderer-contributions' } + ); + + try { + const rectRenderer = getSelectedRectRenderer(stage); + const splitRectSources = getSplitRectContributionSources(rectRenderer); + + expect(splitRectSources.some(source => source.includes('strokeArrayColor'))).toBe(true); + } finally { + stage.release(); + releaseAppRef(); + } + }); + + test('uses the VTable group renderer contributions for app-scoped stages', () => { + const canvas = document.createElement('canvas'); + const { app, stage, releaseAppRef } = createStageFromVRenderApp( + { + canvas, + width: 100, + height: 100 + }, + { mode: 'browser', scope: 'unit-renderer-contributions' } + ); + + try { + const groupRenderer = app.registry.renderer + .getAll() + .find(renderer => renderer.constructor?.name === 'DefaultCanvasGroupRender') as RendererWithGroupContributions; + + groupRenderer?.reInit?.(); + + const contributionNames = groupRenderer?._groupRenderContribitions?.map( + contribution => contribution.constructor?.name + ); + + expect(contributionNames).toContain('SplitGroupAfterRenderContribution'); + } finally { + stage.release(); + releaseAppRef(); + } + }); + + test('uses the VTable image renderer contributions for app-scoped stages', () => { + const canvas = document.createElement('canvas'); + const { app, stage, releaseAppRef } = createStageFromVRenderApp( + { + canvas, + width: 100, + height: 100 + }, + { mode: 'browser', scope: 'unit-image-renderer-contributions' } + ); + + try { + const imageRenderer = app.registry.renderer + .getAll() + .find(renderer => renderer.constructor?.name === 'DefaultCanvasImageRender') as + | RendererWithRenderContributions + | undefined; + + const contributionNames = imageRenderer?._renderContribitions?.map( + contribution => contribution.constructor?.name + ); + + expect(contributionNames).toContain('BeforeImageRenderContribution'); + expect(contributionNames).toContain('AfterImageRenderContribution'); + } finally { + stage.release(); + releaseAppRef(); + } + }); + + test('uses the VTable text renderer contributions for app-scoped stages', () => { + const canvas = document.createElement('canvas'); + const { app, stage, releaseAppRef } = createStageFromVRenderApp( + { + canvas, + width: 100, + height: 100 + }, + { mode: 'browser', scope: 'unit-text-renderer-contributions' } + ); + + try { + const textRenderer = app.registry.renderer + .getAll() + .find(renderer => renderer.constructor?.name === 'DefaultCanvasTextRender') as + | RendererWithRenderContributions + | undefined; + + const contributionNames = textRenderer?._renderContribitions?.map(contribution => contribution.constructor?.name); + + expect(contributionNames).toContain('SuffixTextBeforeRenderContribution'); + } finally { + stage.release(); + releaseAppRef(); + } + }); + + test('installs poptipForText for app-scoped stages', () => { + const canvas = document.createElement('canvas'); + const { stage, releaseAppRef } = createStageFromVRenderApp( + { + canvas, + width: 100, + height: 100, + pluginList: ['poptipForText'] + }, + { mode: 'browser', scope: 'unit-poptip-plugin' } + ); + + try { + expect(stage.pluginService.findPluginsByName('poptipForText').length).toBeGreaterThan(0); + } finally { + stage.release(); + releaseAppRef(); + } + }); + + test('uses the VTable chart picker contribution for app-scoped stages', () => { + const canvas = document.createElement('canvas'); + const { app, stage, releaseAppRef } = createStageFromVRenderApp( + { + canvas, + width: 100, + height: 100 + }, + { mode: 'browser', scope: 'unit-picker-contributions' } + ); + + try { + const picker = app.registry.picker + .getAll() + .find(entry => entry.constructor?.name === 'VChartPicker') as unknown as PickerWithContains | undefined; + + expect(picker?.type).toBe('chart'); + expect( + picker?.contains?.( + { + attribute: {}, + AABBBounds: { + containsPoint: () => true + } + }, + { x: 1, y: 1 } + ) + ).toBe(true); + } finally { + stage.release(); + releaseAppRef(); + } + }); + + test('uses the VTable fast text measure contribution with app-scoped setup', () => { + const canvas = document.createElement('canvas'); + const { stage, releaseAppRef } = createStageFromVRenderApp( + { + canvas, + width: 100, + height: 100 + }, + { mode: 'browser', scope: 'unit-text-measure-contribution' } + ); + + try { + const textMeasure = container.get<{ + constructor?: { name?: string }; + measureTextWidth: (text: string, options: { fontSize: number; fontFamily: string }) => number; + }>(TextMeasureContribution); + + expect(textMeasure.constructor?.name).toBe('FastTextMeasureContribution'); + expect(textMeasure.measureTextWidth('VTable', { fontSize: 12, fontFamily: 'Arial' })).toBeGreaterThan(0); + } finally { + stage.release(); + releaseAppRef(); + } + }); + + test('reactivates the runtime env when reusing an existing shared app', () => { + const firstCanvas = document.createElement('canvas'); + const first = createStageFromVRenderApp( + { + canvas: firstCanvas, + width: 100, + height: 100 + }, + { mode: 'browser', scope: 'unit-env-reactivation' } + ); + + try { + const runtimeGlobal = application.global as { envContribution?: { loadSvg?: unknown } }; + const legacyGlobal = vglobal as { envContribution?: { addEventListener?: unknown } }; + runtimeGlobal.envContribution = undefined; + legacyGlobal.envContribution = undefined; + + const secondCanvas = document.createElement('canvas'); + const second = createStageFromVRenderApp( + { + canvas: secondCanvas, + width: 100, + height: 100 + }, + { mode: 'browser', scope: 'unit-env-reactivation' } + ); + + try { + expect(typeof runtimeGlobal.envContribution?.loadSvg).toBe('function'); + expect(typeof legacyGlobal.envContribution?.addEventListener).toBe('function'); + } finally { + second.stage.release(); + second.releaseAppRef(); + } + } finally { + first.stage.release(); + first.releaseAppRef(); + } + }); + + test('normalizes desktop-browser mode before activating legacy env', () => { + const canvas = document.createElement('canvas'); + const created = createStageFromVRenderApp( + { + canvas, + width: 100, + height: 100 + }, + { mode: 'desktop-browser' as never, scope: 'unit-desktop-browser-env' } + ); + + try { + const legacyGlobal = vglobal as { + env?: string; + envContribution?: { addEventListener?: unknown }; + }; + + expect(legacyGlobal.env).toBe('browser'); + expect(typeof legacyGlobal.envContribution?.addEventListener).toBe('function'); + } finally { + created.stage.release(); + created.releaseAppRef(); + } + }); +}); diff --git a/packages/vtable/__tests__/vrender-app.test.ts b/packages/vtable/__tests__/vrender-app.test.ts new file mode 100644 index 0000000000..64d7825b26 --- /dev/null +++ b/packages/vtable/__tests__/vrender-app.test.ts @@ -0,0 +1,288 @@ +jest.mock('@visactor/vrender/entries/shared', () => ({ + acquireSharedVRenderApp: jest.fn() +})); +jest.mock('@visactor/vrender-components', () => ({ + installPoptipToApp: jest.fn(), + loadPoptip: jest.fn() +})); +jest.mock('../src/scenegraph/runtime-contributions', () => ({ + installVTableRuntimeContributions: jest.fn() +})); + +import { acquireSharedVRenderApp } from '@visactor/vrender/entries/shared'; +import { installPoptipToApp } from '@visactor/vrender-components'; +import type { IApp, IStage } from '@visactor/vrender-core'; +import { createStageFromVRenderApp } from '../src/vrender-app'; +import { installVTableRuntimeContributions } from '../src/scenegraph/runtime-contributions'; + +const mockedAcquireSharedVRenderApp = acquireSharedVRenderApp as jest.Mock; +const mockedInstallPoptipToApp = installPoptipToApp as jest.Mock; +const mockedInstallVTableRuntimeContributions = installVTableRuntimeContributions as jest.Mock; + +const sharedRecords = new Map; refCount: number }>(); +const queuedApps: ReturnType[] = []; + +function createMockApp() { + const app = { + released: false, + createStage: jest.fn((params: unknown) => ({ + params, + release: jest.fn() + })), + release: jest.fn(() => { + app.released = true; + }) + }; + + return app; +} + +function getSharedRecordKey(options: { env: string; key?: string | symbol }) { + return `${options.env}:${String(options.key ?? 'default')}`; +} + +function queueSharedApps(...apps: ReturnType[]) { + queuedApps.push(...apps); +} + +function installSharedAcquireMock() { + mockedAcquireSharedVRenderApp.mockImplementation((options: { env: string; key?: string | symbol }) => { + const key = getSharedRecordKey(options); + let record = sharedRecords.get(key); + + if (!record || record.app.released) { + record = { + app: queuedApps.shift() ?? createMockApp(), + refCount: 0 + }; + sharedRecords.set(key, record); + } + + record.refCount += 1; + let released = false; + + return { + app: record.app, + env: options.env, + key: options.key ?? 'default', + release: jest.fn(() => { + if (released) { + return; + } + released = true; + record.refCount -= 1; + + if (record.refCount <= 0) { + sharedRecords.delete(key); + if (!record.app.released) { + record.app.release(); + } + } + }) + }; + }); +} + +describe('VRender app-scoped stage helper', () => { + beforeEach(() => { + mockedAcquireSharedVRenderApp.mockReset(); + mockedInstallPoptipToApp.mockReset(); + mockedInstallVTableRuntimeContributions.mockReset(); + sharedRecords.clear(); + queuedApps.length = 0; + installSharedAcquireMock(); + }); + + it('reuses the shared app by env and scope until every retained stage is released', () => { + const app = createMockApp(); + queueSharedApps(app); + + const first = createStageFromVRenderApp({ width: 100 }, { mode: 'browser', scope: 'unit-reuse' }); + const second = createStageFromVRenderApp({ width: 200 }, { mode: 'browser', scope: 'unit-reuse' }); + + expect(mockedAcquireSharedVRenderApp).toHaveBeenCalledTimes(2); + expect(mockedAcquireSharedVRenderApp).toHaveBeenNthCalledWith(1, { + env: 'browser', + key: 'browser:unit-reuse:default' + }); + expect(mockedAcquireSharedVRenderApp).toHaveBeenNthCalledWith(2, { + env: 'browser', + key: 'browser:unit-reuse:default' + }); + expect(app.createStage).toHaveBeenCalledTimes(2); + expect(mockedInstallPoptipToApp).toHaveBeenCalledTimes(2); + expect(mockedInstallPoptipToApp).toHaveBeenNthCalledWith(1, app); + expect(mockedInstallPoptipToApp).toHaveBeenNthCalledWith(2, app); + expect(first.stage).not.toBe(second.stage); + + first.releaseAppRef(); + expect(app.release).not.toHaveBeenCalled(); + + second.releaseAppRef(); + second.releaseAppRef(); + expect(app.release).toHaveBeenCalledTimes(1); + }); + + it('keeps fallback apps isolated by scope', () => { + const firstApp = createMockApp(); + const secondApp = createMockApp(); + queueSharedApps(firstApp, secondApp); + + const first = createStageFromVRenderApp({ width: 100 }, { mode: 'browser', scope: 'unit-scope-a' }); + const second = createStageFromVRenderApp({ width: 100 }, { mode: 'browser', scope: 'unit-scope-b' }); + + expect(mockedAcquireSharedVRenderApp).toHaveBeenCalledTimes(2); + expect(mockedAcquireSharedVRenderApp).toHaveBeenNthCalledWith(1, { + env: 'browser', + key: 'browser:unit-scope-a:default' + }); + expect(mockedAcquireSharedVRenderApp).toHaveBeenNthCalledWith(2, { + env: 'browser', + key: 'browser:unit-scope-b:default' + }); + expect(firstApp.createStage).toHaveBeenCalledTimes(1); + expect(secondApp.createStage).toHaveBeenCalledTimes(1); + + first.releaseAppRef(); + second.releaseAppRef(); + expect(firstApp.release).toHaveBeenCalledTimes(1); + expect(secondApp.release).toHaveBeenCalledTimes(1); + }); + + it('uses the shared entry with envParams for node mode', () => { + const app = createMockApp(); + const envParams = { createCanvas: jest.fn() }; + queueSharedApps(app); + + const created = createStageFromVRenderApp({ width: 100 }, { mode: 'node', scope: 'unit-node', envParams }); + + expect(mockedAcquireSharedVRenderApp).toHaveBeenCalledTimes(1); + expect(mockedAcquireSharedVRenderApp).toHaveBeenCalledWith( + expect.objectContaining({ + env: 'node', + key: expect.stringMatching(/^node:unit-node:object:\d+$/), + envParams + }) + ); + expect(app.createStage).toHaveBeenCalledWith({ width: 100 }); + + created.releaseAppRef(); + expect(app.release).toHaveBeenCalledTimes(1); + }); + + it('keeps fallback node apps isolated by envParams identity', () => { + const firstApp = createMockApp(); + const secondApp = createMockApp(); + const firstEnvParams = { createCanvas: jest.fn() }; + const secondEnvParams = { createCanvas: jest.fn() }; + queueSharedApps(firstApp, secondApp); + + const first = createStageFromVRenderApp( + { width: 100 }, + { mode: 'node', scope: 'unit-node-envparams', envParams: firstEnvParams } + ); + const second = createStageFromVRenderApp( + { width: 100 }, + { mode: 'node', scope: 'unit-node-envparams', envParams: secondEnvParams } + ); + + expect(mockedAcquireSharedVRenderApp).toHaveBeenCalledTimes(2); + const firstKey = mockedAcquireSharedVRenderApp.mock.calls[0][0].key; + const secondKey = mockedAcquireSharedVRenderApp.mock.calls[1][0].key; + expect(firstKey).toEqual(expect.stringMatching(/^node:unit-node-envparams:object:\d+$/)); + expect(secondKey).toEqual(expect.stringMatching(/^node:unit-node-envparams:object:\d+$/)); + expect(firstKey).not.toBe(secondKey); + expect(firstApp.createStage).toHaveBeenCalledTimes(1); + expect(secondApp.createStage).toHaveBeenCalledTimes(1); + + first.releaseAppRef(); + second.releaseAppRef(); + expect(firstApp.release).toHaveBeenCalledTimes(1); + expect(secondApp.release).toHaveBeenCalledTimes(1); + }); + + it('uses the shared entry for stable mini modes', () => { + const app = createMockApp(); + const envParams = { canvasIdLists: ['unit'] }; + queueSharedApps(app); + + const created = createStageFromVRenderApp({ width: 100 }, { mode: 'wx', scope: 'unit-wx', envParams }); + + expect(mockedAcquireSharedVRenderApp).toHaveBeenCalledTimes(1); + expect(mockedAcquireSharedVRenderApp).toHaveBeenCalledWith( + expect.objectContaining({ + env: 'wx', + key: expect.stringMatching(/^wx:unit-wx:object:\d+$/), + envParams + }) + ); + expect(app.createStage).toHaveBeenCalledWith({ width: 100 }); + + created.releaseAppRef(); + expect(app.release).toHaveBeenCalledTimes(1); + }); + + it('normalizes VChart desktop-browser mode to the browser VRender app env', () => { + const app = createMockApp(); + queueSharedApps(app); + + const created = createStageFromVRenderApp( + { width: 100 }, + { mode: 'desktop-browser' as never, scope: 'unit-desktop-browser' } + ); + + expect(mockedAcquireSharedVRenderApp).toHaveBeenCalledTimes(1); + expect(mockedAcquireSharedVRenderApp).toHaveBeenCalledWith({ + env: 'browser', + key: 'browser:unit-desktop-browser:default' + }); + expect(app.createStage).toHaveBeenCalledWith({ width: 100 }); + + created.releaseAppRef(); + expect(app.release).toHaveBeenCalledTimes(1); + }); + + it('borrows an externally supplied app and only creates a VTable-owned stage from it', () => { + const app = createMockApp(); + + const created = createStageFromVRenderApp({ width: 100 }, { app: app as unknown as IApp }); + + expect(created.app).toBe(app); + expect(created.stageOwned).toBe(true); + expect(created.appOwned).toBe(false); + expect(mockedInstallVTableRuntimeContributions).toHaveBeenCalledWith(app); + expect(mockedInstallVTableRuntimeContributions.mock.invocationCallOrder[0]).toBeLessThan( + app.createStage.mock.invocationCallOrder[0] + ); + expect(app.createStage).toHaveBeenCalledWith({ width: 100 }); + + created.releaseAppRef(); + expect(app.release).not.toHaveBeenCalled(); + }); + + it('borrows an externally supplied stage without creating or releasing an app', () => { + const stage = { release: jest.fn() } as unknown as IStage; + + const created = createStageFromVRenderApp({ width: 100 }, { stage }); + + expect(created.stage).toBe(stage); + expect(created.stageOwned).toBe(false); + expect(created.appOwned).toBe(false); + expect(created.releaseAppRef()).toBeUndefined(); + expect(stage.release).not.toHaveBeenCalled(); + expect(mockedAcquireSharedVRenderApp).not.toHaveBeenCalled(); + expect(mockedInstallVTableRuntimeContributions).not.toHaveBeenCalled(); + }); + + it('releases the retained fallback app when stage creation throws', () => { + const app = createMockApp(); + const error = new Error('stage failed'); + app.createStage.mockImplementationOnce(() => { + throw error; + }); + queueSharedApps(app); + + expect(() => createStageFromVRenderApp({ width: 100 }, { mode: 'browser', scope: 'unit-error' })).toThrow(error); + expect(app.release).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/vtable/examples/custom-layout-jsx/pivot-jsx.tsx b/packages/vtable/examples/custom-layout-jsx/pivot-jsx.tsx index 3edfdceaa5..a4365a5391 100644 --- a/packages/vtable/examples/custom-layout-jsx/pivot-jsx.tsx +++ b/packages/vtable/examples/custom-layout-jsx/pivot-jsx.tsx @@ -594,16 +594,14 @@ function customLayout(args: VTable.TYPES.CustomRenderFunctionArg) { boundsPadding: [10, 0, 0, 0], cursor: 'pointer' }} - stateProxy={(stateName: string) => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { @@ -734,16 +732,14 @@ function customLayout(args: VTable.TYPES.CustomRenderFunctionArg) { boundsPadding: [0, 0, 0, 5], cursor: 'pointer' }} - stateProxy={(stateName: string) => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { @@ -782,16 +778,14 @@ function customLayout(args: VTable.TYPES.CustomRenderFunctionArg) { boundsPadding: [12, 0, 0, 5], cursor: 'pointer' }} - stateProxy={(stateName: string) => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { @@ -814,16 +808,14 @@ function customLayout(args: VTable.TYPES.CustomRenderFunctionArg) { boundsPadding: [8, 0, 0, 5], cursor: 'pointer' }} - stateProxy={(stateName: string) => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { @@ -938,16 +930,14 @@ function customLayoutRow(args: VTable.TYPES.CustomRenderFunctionArg) { height: 20, cursor: 'pointer' }} - stateProxy={(stateName: string) => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { @@ -983,16 +973,14 @@ function customLayoutRow(args: VTable.TYPES.CustomRenderFunctionArg) { boundsPadding: [0, 10, 0, 0], cursor: 'pointer' }} - stateProxy={(stateName: string) => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { @@ -1015,16 +1003,14 @@ function customLayoutRow(args: VTable.TYPES.CustomRenderFunctionArg) { visible: false, cursor: 'pointer' }} - stateProxy={(stateName: string) => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { @@ -1086,16 +1072,14 @@ function customLayoutIndicator(args: VTable.TYPES.CustomRenderFunctionArg) { boundsPadding: [0, 5, 0, 0], cursor: 'pointer' }} - stateProxy={(stateName: string) => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; + states={{ + hover: { + background: { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } } }} onMouseEnter={event => { diff --git a/packages/vtable/examples/debug/header-frame-border-null-color.ts b/packages/vtable/examples/debug/header-frame-border-null-color.ts new file mode 100644 index 0000000000..7cb38efa66 --- /dev/null +++ b/packages/vtable/examples/debug/header-frame-border-null-color.ts @@ -0,0 +1,252 @@ +import * as VTable from '../../src'; + +const CONTAINER_ID = 'vTable'; +const BORDER_COLOR = '#E1E4E8'; +const FONT_FAMILY = + '-apple-system,"Helvetica Neue","PingFang SC",Arial,"Microsoft YaHei","Hiragino Sans GB",Helvetica,' + + 'sans-serif,"apple color emoji","segoe ui emoji","segoe ui","segoe ui symbol"'; + +const headerFrameStyle = { + borderColor: [null, null, BORDER_COLOR, null], + borderLineWidth: 2 +}; + +const baseStyle = { + borderColor: [BORDER_COLOR, BORDER_COLOR, BORDER_COLOR, BORDER_COLOR], + borderLineWidth: [1, 1, 1, 1], + borderLineDash: [null, null, null, null], + padding: [8.6, 12, 8.6, 12], + fontFamily: FONT_FAMILY, + fontSize: 12, + fontStyle: 'normal', + fontVariant: 'normal', + lineHeight: 18, + underline: false +}; + +const bodyStyle = { + ...baseStyle, + hover: { + cellBgColor: 'rgba(186, 215, 255, 0.7)', + inlineRowBgColor: 'rgba(186, 215, 255, 0.3)', + inlineColumnBgColor: 'rgba(186, 215, 255, 0.3)' + }, + fontWeight: 'normal', + color: '#141414' +}; + +const headerStyle = { + ...baseStyle, + hover: { + cellBgColor: 'rgba(0, 100, 250, 0.16)', + inlineRowBgColor: 'rgba(255, 255, 255, 0)', + inlineColumnBgColor: 'rgba(255, 255, 255, 0)' + }, + frameStyle: headerFrameStyle, + fontWeight: 'bold', + color: '#1b1f23', + bgColor: '#eef1f5' +}; + +const columns: VTable.TYPES.ColumnsDefine = [ + { + field: 'category', + title: '类别', + showSort: false, + style: { ...bodyStyle, textAlign: 'left' }, + headerStyle: { ...headerStyle, textAlign: 'left' }, + width: 76, + cellType: 'link', + linkDetect: true, + linkJump: false + }, + { + field: 'subCategory', + title: '子类别', + showSort: false, + style: { ...bodyStyle, textAlign: 'left' }, + headerStyle: { ...headerStyle, textAlign: 'left' }, + width: 188, + cellType: 'link', + linkDetect: true, + linkJump: false + }, + { + field: 'manager', + title: '地区经理', + showSort: false, + style: { ...bodyStyle, textAlign: 'left' }, + headerStyle: { ...headerStyle, textAlign: 'left' }, + width: 76, + cellType: 'link', + linkDetect: true, + linkJump: false + }, + { + field: 'sales', + title: '销售额', + showSort: false, + style: { ...bodyStyle, textAlign: 'right' }, + headerStyle: { ...headerStyle, textAlign: 'right' }, + width: 88, + dropDownMenu: [ + { text: '降序排序', menuKey: 'sort_desc' }, + { text: '升序排序', menuKey: 'sort_asc' }, + { text: '冻结列', menuKey: 'frozen_col' } + ] + }, + { + field: 'quantity', + title: '数量', + showSort: false, + style: { ...bodyStyle, textAlign: 'right' }, + headerStyle: { ...headerStyle, textAlign: 'right' }, + width: 729 + }, + { + field: 'profit', + title: '利润', + showSort: false, + style: { ...bodyStyle, textAlign: 'right' }, + headerStyle: { ...headerStyle, textAlign: 'right' }, + width: 460 + } +]; + +const records = [ + { + category: '家具', + subCategory: '椅子', + manager: '杨健', + sales: '153884.02499389648', + quantity: '184', + profit: '26845.084301948547' + }, + { + category: '办公用品', + subCategory: '系固件', + manager: '杨健', + sales: '4729.787956237793', + quantity: '85', + profit: '775.6280167102814' + }, + { + category: '家具', + subCategory: '书架', + manager: '白德伟', + sales: '228510.26754760742', + quantity: '253', + profit: '29967.308319091797' + }, + { + category: '办公用品', + subCategory: '装订机', + manager: '楚杰', + sales: '45179.95973491669', + quantity: '517', + profit: '4118.6599479317665' + }, + { + category: '家具', + subCategory: '桌子', + manager: '楚杰', + sales: '184567.76220703125', + quantity: '130', + profit: '-15772.939086914062' + } +]; + +function createInfo(root: HTMLElement) { + const info = document.createElement('div'); + info.style.cssText = [ + 'box-sizing: border-box', + 'padding: 8px 12px', + 'font: 13px/1.5 sans-serif', + 'color: #24292f', + 'background: #f6f8fa', + 'border-bottom: 1px solid #d0d7de' + ].join(';'); + info.innerHTML = [ + 'Header frameStyle null borderColor regression case.', + 'Only the bottom header frame border should be visible.', + 'borderColor: [null, null, "#E1E4E8", null] is intentional.' + ].join(' '); + root.appendChild(info); +} + +export function createTable() { + const root = document.getElementById(CONTAINER_ID) as HTMLElement; + root.innerHTML = ''; + root.style.display = 'flex'; + root.style.flexDirection = 'column'; + + createInfo(root); + + const tableContainer = document.createElement('div'); + tableContainer.style.cssText = 'position: relative; flex: 1; min-height: 0; width: 100%; overflow: hidden;'; + root.appendChild(tableContainer); + + const option: VTable.ListTableConstructorOptions = { + container: tableContainer, + columns, + records, + transpose: false, + widthMode: 'standard', + columnResizeMode: 'all', + heightMode: 'autoHeight', + autoWrapText: false, + maxCharactersNumber: 256, + defaultHeaderColWidth: 'auto', + keyboardOptions: { + selectAllOnCtrlA: true, + copySelected: false + }, + menu: { + renderMode: 'html', + dropDownMenuHighlight: [ + { + menuKey: 'frozen_col', + field: 'sales' + } + ] + }, + customConfig: { + _disableColumnAndRowSizeRound: true, + imageMargin: 4, + multilinesForXTable: true, + shrinkSparklineFirst: true + }, + frozenColCount: 4, + theme: { + underlayBackgroundColor: 'rgba(255,255,255,0)', + frameStyle: { + borderColor: BORDER_COLOR, + borderLineWidth: 1 + }, + headerStyle, + rowHeaderStyle: headerStyle, + bodyStyle, + frozenColumnLine: { + shadow: { + width: 3, + startColor: 'rgba(225, 228, 232, 0.6)', + endColor: 'rgba(225, 228, 232, 0.6)' + } + }, + cellInnerBorder: false, + cellBorderClipDirection: 'bottom-right', + _contentOffset: 1 + }, + showHeader: true, + hover: { + highlightMode: 'row' + }, + select: { + highlightMode: 'cell', + headerSelectMode: 'inline' + } + }; + + const table = new VTable.ListTable(option); + (window as unknown as { tableInstance?: VTable.ListTable }).tableInstance = table; +} diff --git a/packages/vtable/examples/list/list-group.ts b/packages/vtable/examples/list/list-group.ts index ea8e8428e0..d082b1ae50 100644 --- a/packages/vtable/examples/list/list-group.ts +++ b/packages/vtable/examples/list/list-group.ts @@ -144,16 +144,14 @@ export function createTable() { // // boundsPadding: [0, 0, 0, 10], // // cursor: 'pointer' // // }); - // // icon.stateProxy = (stateName: string) => { - // // if (stateName === 'hover') { - // // return { - // // background: { - // // fill: '#ccc', - // // cornerRadius: 5, - // // expandX: 1, - // // expandY: 1 - // // } - // // }; + // // icon.states = { + // // hover: { + // // background: { + // // fill: '#ccc', + // // cornerRadius: 5, + // // expandX: 1, + // // expandY: 1 + // // } // // } // // }; // // icon.addEventListener('pointerenter', event => { @@ -261,16 +259,14 @@ export function createTable() { // boundsPadding: [0, 0, 0, 10], // cursor: 'pointer' // }); - // icon.stateProxy = (stateName: string) => { - // if (stateName === 'hover') { - // return { - // background: { - // fill: '#ccc', - // cornerRadius: 5, - // expandX: 1, - // expandY: 1 - // } - // }; + // icon.states = { + // hover: { + // background: { + // fill: '#ccc', + // cornerRadius: 5, + // expandX: 1, + // expandY: 1 + // } // } // }; // icon.addEventListener('pointerenter', event => { diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index f9195f1ab6..5f040cb70a 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -49,6 +49,10 @@ export const menus = [ { path: 'debug', name: 'issue-5187-max-frozen-width' + }, + { + path: 'debug', + name: 'header-frame-border-null-color' } ] }, diff --git a/packages/vtable/jest.config.js b/packages/vtable/jest.config.js index 26480d1ffe..3703124804 100644 --- a/packages/vtable/jest.config.js +++ b/packages/vtable/jest.config.js @@ -1,5 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-var-requires const path = require('path'); +const { createVRenderModuleNameMapper } = require('../../common/config/jest/vrender-module-name-mapper'); module.exports = { preset: 'ts-jest', @@ -48,6 +49,7 @@ module.exports = { 'd3-dsv': path.resolve(__dirname, './node_modules/d3-dsv/dist/d3-dsv.min.js'), 'd3-hexbin': path.resolve(__dirname, './node_modules/d3-hexbin/build/d3-hexbin.min.js'), 'd3-hierarchy': path.resolve(__dirname, './node_modules/d3-hierarchy/dist/d3-hierarchy.min.js'), + ...createVRenderModuleNameMapper(), // '@visactor/vtable-editors': path.resolve(__dirname, '../vtable-editors/src/index.ts'), // '@visactor/vtable-plugins': path.resolve(__dirname, '../vtable-plugins/src/index.ts'), '@visactor/vtable-editors': '/../vtable-editors/src/index.ts', diff --git a/packages/vtable/package.json b/packages/vtable/package.json index 86dcb61662..59b0bd3de2 100644 --- a/packages/vtable/package.json +++ b/packages/vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable", - "version": "1.26.4", + "version": "1.26.5", "description": "canvas table width high performance", "keywords": [ "grid", @@ -52,11 +52,12 @@ "fix-memory-limit": "cross-env LIMIT=10240 increase-memory-limit" }, "dependencies": { - "@visactor/vtable-editors": "workspace:1.26.4", - "@visactor/vrender-core": "~1.0.41", - "@visactor/vrender-kits": "~1.0.41", - "@visactor/vrender-components": "~1.0.41", - "@visactor/vrender-animate": "~1.0.41", + "@visactor/vtable-editors": "workspace:1.26.5", + "@visactor/vrender": "1.1.4", + "@visactor/vrender-core": "1.1.4", + "@visactor/vrender-kits": "1.1.4", + "@visactor/vrender-components": "1.1.4", + "@visactor/vrender-animate": "1.1.4", "@visactor/vutils": "~1.0.17", "@visactor/vscale": "~1.0.17", "@visactor/vdataset": "~1.0.17", @@ -69,7 +70,7 @@ "cross-env": "^7.0.3", "increase-memory-limit": "^1.0.7", "luxon": "*", - "@visactor/vchart": "2.0.13-alpha.10", + "@visactor/vchart": "2.1.3", "@internal/bundler": "workspace:*", "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index 070c74ee65..c11e7b0610 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -84,6 +84,17 @@ import { // registerVideoCell // } from './scenegraph/group-creater/cell-type'; +const LAYOUT_COLUMN_STATE_KEYS = ['level', 'startIndex', 'id', 'levelSpan', 'size', 'startInTotal', 'hierarchyState']; + +function clearLayoutColumnState(columns: ColumnsDefine | undefined) { + columns?.forEach(column => { + LAYOUT_COLUMN_STATE_KEYS.forEach(key => { + delete (column as any)[key]; + }); + clearLayoutColumnState((column as any).children ?? (column as any).columns); + }); +} + // registerAxis(); // registerEmptyTip(); // registerLegend(); @@ -1062,26 +1073,30 @@ export class ListTable extends BaseTable implements ListTableAPI { return null; } private syncColumnsStateFromLayoutMap() { - const nextColumns = cloneDeepSpec(this.internalProps.columns, ['children']); - const cleanColumns = (columns: any[]) => { - columns.forEach(col => { - delete col.level; - delete col.size; - delete col.startInTotal; - delete col.startIndex; - delete col.id; - delete col.hierarchyState; - if (col.columns) { - cleanColumns(col.columns); - } - }); - }; - cleanColumns(nextColumns); - this.options.columns = nextColumns; + const sourceColumns = this.options.columns ?? this.internalProps.columns; + const nextColumns = sourceColumns.some(column => column.hide === true) + ? this.mergeHiddenColumnsWithVisibleOrder(sourceColumns) + : this.columns; + const publicColumns = cloneDeepSpec(nextColumns, ['children']); + clearLayoutColumnState(publicColumns); + this.internalProps.columns = cloneDeepSpec(publicColumns, ['children']); + this.options.columns = publicColumns; if (this.options.header) { - this.options.header = nextColumns; + this.options.header = publicColumns; } } + private mergeHiddenColumnsWithVisibleOrder(sourceColumns: ColumnsDefine) { + const visibleColumns = this.internalProps.layoutMap.columnObjects.map(column => column.define); + let visibleIndex = 0; + return sourceColumns.map(column => { + if (column.hide === true) { + return column; + } + const nextVisibleColumn = visibleColumns[visibleIndex]; + visibleIndex += 1; + return nextVisibleColumn ?? column; + }); + } changeRecordOrder(sourceIndex: number, targetIndex: number) { if (this.transpose) { sourceIndex = this.getRecordShowIndexByCell(sourceIndex, 0); diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts index be40871fb1..7230e26571 100644 --- a/packages/vtable/src/core/BaseTable.ts +++ b/packages/vtable/src/core/BaseTable.ts @@ -267,7 +267,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { if (Env.mode === 'node') { options = container as BaseTableConstructorOptions; container = null; - } else if (!(container instanceof HTMLElement)) { + } else if (container && !(container instanceof HTMLElement)) { options = container as BaseTableConstructorOptions; if ((container as BaseTableConstructorOptions).container) { container = (container as BaseTableConstructorOptions).container; @@ -2507,13 +2507,16 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { const frozenColsOffset = this.getFrozenColsOffset(); const bottomFrozenRowsHeight = this.getBottomFrozenRowsHeight(); const rightFrozenColsWidth = this.getRightFrozenColsWidth(); + const frozenHeaderHeight = + this.frozenRowCount > this.columnHeaderLevelCount ? this.getRowsHeight(0, this.columnHeaderLevelCount - 1) : 0; // 计算非冻结 const rowStart = Math.max(this.getTargetRowAt(scrollTop + 1)?.row ?? -1, this.frozenRowCount); const { col: colStart } = this.getColAt(scrollLeft + frozenColsContentWidth + 1); const rowEnd = this.getAllRowsHeight() > this.tableNoFrameHeight ? Math.max( - this.getTargetRowAt(scrollTop + this.tableNoFrameHeight - 1 - bottomFrozenRowsHeight)?.row ?? -1, + this.getTargetRowAt(scrollTop + this.tableNoFrameHeight - 1 - bottomFrozenRowsHeight - frozenHeaderHeight) + ?.row ?? -1, rowStart ) : this.rowCount - 1; @@ -2535,13 +2538,16 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { getBodyVisibleRowRange(start_deltaY: number = 0, end_deltaY: number = 0) { const { scrollTop } = this; const bottomFrozenRowsHeight = this.getBottomFrozenRowsHeight(); + const frozenHeaderHeight = + this.frozenRowCount > this.columnHeaderLevelCount ? this.getRowsHeight(0, this.columnHeaderLevelCount - 1) : 0; // 计算非冻结 const rowStart = Math.max(this.getTargetRowAt(scrollTop + 1 + start_deltaY)?.row ?? -1, this.frozenRowCount); const rowEnd = this.getAllRowsHeight() > this.tableNoFrameHeight ? Math.max( - this.getTargetRowAt(scrollTop + this.tableNoFrameHeight - 1 - bottomFrozenRowsHeight + end_deltaY)?.row ?? - -1, + this.getTargetRowAt( + scrollTop + this.tableNoFrameHeight - 1 - bottomFrozenRowsHeight - frozenHeaderHeight + end_deltaY + )?.row ?? -1, rowStart ) : this.rowCount - 1; @@ -2700,7 +2706,9 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { this.scenegraph?.component?.hScrollBar?.release(); this.animationManager.clear(); this.animationManager.ticker.release(); - this.scenegraph?.stage?.ticker?.release(); + if (this.scenegraph?.stageOwned) { + this.scenegraph?.stage?.ticker?.release(); + } const internalProps = this.internalProps; const canvas = internalProps?.canvas as any; @@ -2736,7 +2744,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { internalProps.releaseList = null; } - this.scenegraph.stage.release(); + this.scenegraph.releaseStage(); this.scenegraph.proxy.release(); const parentElement = internalProps.element?.parentElement; diff --git a/packages/vtable/src/scenegraph/animation/appear.ts b/packages/vtable/src/scenegraph/animation/appear.ts index 5ed3bd7209..e1199815f4 100644 --- a/packages/vtable/src/scenegraph/animation/appear.ts +++ b/packages/vtable/src/scenegraph/animation/appear.ts @@ -1,6 +1,32 @@ import type { Text } from '@src/vrender'; import type { BaseTableAPI } from '../../ts-types/base-table'; +type OpacityAttribute = { + opacity?: Text['attribute']['opacity']; +}; + +type AppearGraphic = Text & { + getFinalAttribute?: () => OpacityAttribute | undefined; + setFinalAttributes?: (attribute: OpacityAttribute) => void; +}; + +function getAppearFinalOpacity(child: AppearGraphic) { + return child.attribute.opacity ?? child.getFinalAttribute?.()?.opacity ?? 1; +} + +function setAppearFinalOpacity(child: AppearGraphic, opacity: number) { + child.setAttribute('opacity', opacity); + + const finalAttribute = child.getFinalAttribute?.(); + if (finalAttribute) { + if (child.setFinalAttributes) { + child.setFinalAttributes({ opacity }); + } else { + finalAttribute.opacity = opacity; + } + } +} + export function dealWithAnimationAppear(table: BaseTableAPI) { if (!table.options.animationAppear) { return; @@ -23,21 +49,21 @@ export function dealWithAnimationAppear(table: BaseTableAPI) { direction = table.options.animationAppear.direction ?? 'row'; } - const { scenegraph: scene, frozenColCount, frozenRowCount } = table; + const { scenegraph: scene } = table; // header cell - const { colStart, colEnd, rowStart, rowEnd } = scene.proxy; // to do: right bottom frozen + const { colEnd, rowEnd } = scene.proxy; // to do: right bottom frozen for (let col = 0; col <= colEnd; col++) { for (let row = 0; row <= rowEnd; row++) { const cellGroup = scene.highPerformanceGetCell(col, row); if (cellGroup && cellGroup.role === 'cell') { - cellGroup.forEachChildren((child: Text) => { - child.setAttribute('opacity', 0); - child - .animate() - .wait(type === 'one-by-one' ? (direction === 'row' ? row : col) * (duration - delay) : delay) - .to({ opacity: 1 }, duration, 'linear'); + cellGroup.forEachChildren((child: AppearGraphic) => { + const finalOpacity = getAppearFinalOpacity(child); + const animationDelay = type === 'one-by-one' ? (direction === 'row' ? row : col) * (duration - delay) : delay; + + setAppearFinalOpacity(child, finalOpacity); + child.animate().wait(animationDelay).from({ opacity: 0 }, duration, 'linear'); }); } } diff --git a/packages/vtable/src/scenegraph/component/custom.ts b/packages/vtable/src/scenegraph/component/custom.ts index 6b4b380ad6..383ea3065d 100644 --- a/packages/vtable/src/scenegraph/component/custom.ts +++ b/packages/vtable/src/scenegraph/component/custom.ts @@ -455,11 +455,14 @@ export function decodeReactDom(dom: any, timeline?: ITimeline) { return dom; } const type = dom.type; - const { attribute, children, stateProxy, animation } = dom.props; + const { attribute, children, states, sharedStateDefinitions, animation } = dom.props; const g = type({ attribute }); parseToGraphic(g, dom.props); - if (stateProxy) { - g.stateProxy = stateProxy; + if (states) { + g.states = states; + } + if (sharedStateDefinitions && g.type === 'group') { + g.sharedStateDefinitions = sharedStateDefinitions; } g.id = attribute.id; diff --git a/packages/vtable/src/scenegraph/component/menu.ts b/packages/vtable/src/scenegraph/component/menu.ts index 3381d0c917..f185b92691 100644 --- a/packages/vtable/src/scenegraph/component/menu.ts +++ b/packages/vtable/src/scenegraph/component/menu.ts @@ -175,16 +175,10 @@ export class MenuHandler { group.role = 'menu-item'; this._menuInstance.appendChild(group); - // 处理背景hover效果 - group.stateProxy = (stateName: string) => { - if (stateName === 'hover') { - return { - fill: menuStyle.hoverBgColor - }; + group.states = { + hover: { + fill: menuStyle.hoverBgColor } - return { - fill: menuStyle.bgColor - }; }; group.addEventListener('pointerenter', (e: FederatedEvent) => { group.addState('hover', true, false); diff --git a/packages/vtable/src/scenegraph/graphic/chart.ts b/packages/vtable/src/scenegraph/graphic/chart.ts index 39c3ccad89..651b1a5f97 100644 --- a/packages/vtable/src/scenegraph/graphic/chart.ts +++ b/packages/vtable/src/scenegraph/graphic/chart.ts @@ -1,4 +1,4 @@ -import type { GraphicType, IGroupGraphicAttribute, Stage, Group } from '@src/vrender'; +import type { GraphicType, IGroupGraphicAttribute, ISetAttributeContext, Stage, Group } from '@src/vrender'; import { genNumberType, Rect } from '@src/vrender'; import { Bounds, merge } from '@visactor/vutils'; import type { BaseTableAPI } from '../../ts-types/base-table'; @@ -45,6 +45,20 @@ interface IChartGraphicAttribute extends IGroupGraphicAttribute { detectPickChartItem?: boolean; } +const CHART_RUNTIME_ATTRIBUTE_KEYS: (keyof IChartGraphicAttribute)[] = [ + 'canvas', + 'modeParams', + 'spec', + 'ClassType', + 'chartInstance', + 'dataId', + 'data', + 'cellPadding', + 'axes', + 'tableChartOption', + 'detectPickChartItem' +]; + export const CHART_NUMBER_TYPE = genNumberType(); export class Chart extends Rect { @@ -61,6 +75,7 @@ export class Chart extends Rect { isShareChartSpec: boolean; //针对chartSpec用户配置成函数形式的话 就不需要存储chartInstance了 会太占内存,使用这个变量 当渲染出缓存图表会就删除chartInstance实例 constructor(isShareChartSpec: boolean, params: IChartGraphicAttribute) { super(params); + this.syncRuntimeAttributes(params); this.numberType = CHART_NUMBER_TYPE; this.isShareChartSpec = isShareChartSpec; // 创建chart @@ -90,8 +105,10 @@ export class Chart extends Rect { chartInstance.renderSync(); chartInstance.getStage().enableDirtyBounds(); params.chartInstance = this.chartInstance = chartInstance; + this.syncRuntimeAttributes({ chartInstance } as Partial); } else { this.chartInstance = params.chartInstance; + this.syncRuntimeAttributes({ chartInstance: params.chartInstance } as Partial); } // this.chart.load().then((cache) => { @@ -100,6 +117,62 @@ export class Chart extends Rect { // }); } + private syncRuntimeAttributes(source: Partial) { + const attribute = this.attribute as Record; + const baseAttributes = this.baseAttributes as Record; + + CHART_RUNTIME_ATTRIBUTE_KEYS.forEach(key => { + if (Object.prototype.hasOwnProperty.call(source, key)) { + attribute[key] = source[key]; + baseAttributes[key] = source[key]; + } + }); + } + + private restoreRuntimeAttributeRefs() { + const attribute = this.attribute as Record; + const baseAttributes = this.baseAttributes as Record; + + CHART_RUNTIME_ATTRIBUTE_KEYS.forEach(key => { + if (Object.prototype.hasOwnProperty.call(baseAttributes, key)) { + attribute[key] = baseAttributes[key]; + } + }); + } + + setAttributes( + params: Partial, + forceUpdateTag?: boolean, + context?: ISetAttributeContext + ): void { + super.setAttributes(params, forceUpdateTag, context); + this.restoreRuntimeAttributeRefs(); + } + + setAttribute(key: string, value: any, forceUpdateTag?: boolean, context?: ISetAttributeContext): void { + super.setAttribute(key, value, forceUpdateTag, context); + this.restoreRuntimeAttributeRefs(); + } + + // Chart attributes carry DOM/VChart runtime references; VRender state snapshots must not deep-clone them. + protected buildStaticAttributeSnapshot(): Partial { + const snapshot = { ...((this.baseAttributes as Record | undefined) ?? {}) }; + const resolvedPatch = this.resolvedStatePatch as Record | undefined; + + resolvedPatch && + Object.keys(resolvedPatch).forEach(key => { + snapshot[key] = resolvedPatch[key]; + }); + + CHART_RUNTIME_ATTRIBUTE_KEYS.forEach(key => { + if (Object.prototype.hasOwnProperty.call(this.attribute, key)) { + snapshot[key] = (this.attribute as Record)[key]; + } + }); + + return snapshot as Partial; + } + // onBeforeAttributeUpdate() { // if (arguments[2] === y) { // debugger; diff --git a/packages/vtable/src/scenegraph/graphic/contributions/chart-render-helper.ts b/packages/vtable/src/scenegraph/graphic/contributions/chart-render-helper.ts index fda4962a30..95dcbd3a6e 100644 --- a/packages/vtable/src/scenegraph/graphic/contributions/chart-render-helper.ts +++ b/packages/vtable/src/scenegraph/graphic/contributions/chart-render-helper.ts @@ -12,7 +12,7 @@ interface chartRenderQueueItem { //每次消费的图表数量 let batchRenderChartCount = 5; let isHandlingChartQueue = false; -let requestAnimationFrameId: number; +let requestAnimationFrameId: number | undefined; export function setBatchRenderChartCount(count: number) { if (isValid(count)) { batchRenderChartCount = count; @@ -22,7 +22,10 @@ export function clearChartRenderQueue() { chartRenderKeys = []; chartRenderQueueList = []; isHandlingChartQueue = false; - vglobal.getCancelAnimationFrame()(requestAnimationFrameId); + if (requestAnimationFrameId != null) { + vglobal.getCancelAnimationFrame()(requestAnimationFrameId); + requestAnimationFrameId = undefined; + } } export function IsHandlingChartQueue() { return isHandlingChartQueue; diff --git a/packages/vtable/src/scenegraph/graphic/group.ts b/packages/vtable/src/scenegraph/graphic/group.ts index fd436b5e59..2647dfdbf4 100644 --- a/packages/vtable/src/scenegraph/graphic/group.ts +++ b/packages/vtable/src/scenegraph/graphic/group.ts @@ -226,21 +226,57 @@ export class Group extends VRenderGroup { return null; } - addCellGroup(cellGroup: Group) { + addCellGroup(cellGroup: Group): Group { + cellGroup = this.getInsertableCellGroup(cellGroup); + if (this.childrenCount === 0 || (this.lastChild as Group).row === cellGroup.row - 1) { this.addChild(cellGroup); + return cellGroup; } else { // for promise cell row order in column let c = this._firstChild as Group; for (let i = 0; i < this.childrenCount; i++) { if (c.row === cellGroup.row - 1) { this.insertAfter(cellGroup, c); - return; + return cellGroup; } c = c._next as Group; } this.addChild(cellGroup); + return cellGroup; + } + } + + private getInsertableCellGroup(cellGroup: Group): Group { + if (cellGroup.isAncestorsOf(this)) { + return this.cloneCellGroupForInsertion(cellGroup); } + + const parent = cellGroup.parent as Group | undefined; + if (parent) { + parent.removeChild(cellGroup); + } + return cellGroup; + } + + private cloneCellGroupForInsertion(cellGroup: Group): Group { + const clonedCellGroup = new Group({ ...cellGroup.attribute }); + clonedCellGroup.name = cellGroup.name; + clonedCellGroup.role = cellGroup.role; + clonedCellGroup.col = cellGroup.col; + clonedCellGroup.row = cellGroup.row; + clonedCellGroup.mergeStartCol = cellGroup.mergeStartCol; + clonedCellGroup.mergeStartRow = cellGroup.mergeStartRow; + clonedCellGroup.mergeEndCol = cellGroup.mergeEndCol; + clonedCellGroup.mergeEndRow = cellGroup.mergeEndRow; + clonedCellGroup.contentWidth = cellGroup.contentWidth; + clonedCellGroup.contentHeight = cellGroup.contentHeight; + clonedCellGroup.rowNumber = cellGroup.rowNumber; + clonedCellGroup.colHeight = cellGroup.colHeight; + clonedCellGroup.needUpdate = cellGroup.needUpdate; + clonedCellGroup.needUpdateWidth = cellGroup.needUpdateWidth; + clonedCellGroup.needUpdateHeight = cellGroup.needUpdateHeight; + return clonedCellGroup; } getChildAt(index: number) { diff --git a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts index 4268b776d6..b443c0b3c3 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts @@ -666,12 +666,12 @@ export function updateCell( isMerge = range.start.col !== range.end.col || range.start.row !== range.end.row; } let isVtableMerge = false; - if (table.internalProps.enableTreeNodeMerge && isMerge) { + if (table.internalProps.enableTreeNodeMerge && range) { const rawRecord = table.getCellRawRecord(range.start.col, range.start.row); const { vtableMergeName, vtableMerge } = rawRecord ?? {}; - isVtableMerge = vtableMerge; - if (vtableMerge) { + isVtableMerge = vtableMerge && (isMerge || !table.isSeriesNumberInBody(col, row)); + if (isVtableMerge) { mayHaveIcon = true; if ((table.internalProps as ListTableProtected).groupTitleCustomLayout) { customResult = dealWithCustom( @@ -712,6 +712,7 @@ export function updateCell( if ( !addNew && !isMerge && + !isVtableMerge && !(define?.customLayout || define?.customRender || define?.headerCustomLayout || define?.headerCustomRender) && (forceFastUpdate || canUseFastUpdate(col, row, oldCellGroup, autoWrapText, mayHaveIcon, table)) ) { diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/button-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/button-cell.ts index c81245c8eb..09bb63974f 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/button-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/button-cell.ts @@ -77,7 +77,7 @@ export function createButtonCellGroup( cellGroup.role = 'cell'; cellGroup.col = col; cellGroup.row = row; - columnGroup?.addCellGroup(cellGroup); + cellGroup = columnGroup?.addCellGroup(cellGroup) ?? cellGroup; } } diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts index fdc78ceb6d..27383d378b 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts @@ -82,7 +82,7 @@ export function createChartCellGroup( cellGroup.role = 'cell'; cellGroup.col = col; cellGroup.row = row; - columnGroup?.addCellGroup(cellGroup); + cellGroup = columnGroup?.addCellGroup(cellGroup) ?? cellGroup; } } cellGroup.AABBBounds.width(); // TODO 需要底层VRender修改 diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/checkbox-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/checkbox-cell.ts index e7bf705025..dcf7aeff55 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/checkbox-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/checkbox-cell.ts @@ -91,7 +91,7 @@ export function createCheckboxCellGroup( cellGroup.role = 'cell'; cellGroup.col = col; cellGroup.row = row; - columnGroup?.addCellGroup(cellGroup); + cellGroup = columnGroup?.addCellGroup(cellGroup) ?? cellGroup; } } diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/image-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/image-cell.ts index 0402bf657b..3e9a570e0c 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/image-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/image-cell.ts @@ -95,7 +95,7 @@ export function createImageCellGroup( cellGroup.role = 'cell'; cellGroup.col = col; cellGroup.row = row; - columnGroup?.addCellGroup(cellGroup); + cellGroup = columnGroup?.addCellGroup(cellGroup) ?? cellGroup; } let cellIcons; diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/radio-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/radio-cell.ts index 791d546f6a..a7012394cc 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/radio-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/radio-cell.ts @@ -58,7 +58,7 @@ export function createRadioCellGroup( cellGroup.col = col; cellGroup.row = row; // columnGroup?.addChild(cellGroup); - columnGroup?.addCellGroup(cellGroup); + cellGroup = columnGroup?.addCellGroup(cellGroup) ?? cellGroup; } // radio diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/spark-line-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/spark-line-cell.ts index ce6d3c1294..259d642314 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/spark-line-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/spark-line-cell.ts @@ -72,7 +72,7 @@ export function createSparkLineCellGroup( cellGroup.role = 'cell'; cellGroup.col = col; cellGroup.row = row; - columnGroup?.addCellGroup(cellGroup); + cellGroup = columnGroup?.addCellGroup(cellGroup) ?? cellGroup; } } diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/switch-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/switch-cell.ts index cdd78b493b..705a4739fa 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/switch-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/switch-cell.ts @@ -77,7 +77,7 @@ export function createSwitchCellGroup( cellGroup.role = 'cell'; cellGroup.col = col; cellGroup.row = row; - columnGroup?.addCellGroup(cellGroup); + cellGroup = columnGroup?.addCellGroup(cellGroup) ?? cellGroup; } } 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 95d79da0a2..8ce3149af2 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 @@ -113,7 +113,7 @@ export function createCellGroup( cellGroup.role = 'cell'; cellGroup.col = col; cellGroup.row = row; - columnGroup?.addCellGroup(cellGroup); + cellGroup = columnGroup?.addCellGroup(cellGroup) ?? cellGroup; } if (customElementsGroup) { cellGroup.appendChild(customElementsGroup); diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/video-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/video-cell.ts index 6fb63e5347..ac058bd6d6 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/video-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/video-cell.ts @@ -187,7 +187,7 @@ export function createVideoCellGroup( cellGroup.role = 'cell'; cellGroup.col = col; cellGroup.row = row; - columnGroup?.addCellGroup(cellGroup); + cellGroup = columnGroup?.addCellGroup(cellGroup) ?? cellGroup; } let cellIcons; @@ -339,8 +339,12 @@ export function createVideoCellGroup( releaseCurrentVideo(); return; } + const scenegraph = table.scenegraph; + if (!scenegraph) { + return; + } if (imageAutoSizing) { - _adjustWidthHeight(col, row, video.videoWidth, video.videoHeight, table.scenegraph, padding, cellGroup); + _adjustWidthHeight(col, row, video.videoWidth, video.videoHeight, scenegraph, padding, cellGroup); } // const width = cellGroup.attribute.width; // const height = cellGroup.attribute.height; @@ -428,7 +432,7 @@ export function createVideoCellGroup( releaseCurrentVideo(); } // 触发重绘 - table.scenegraph.updateNextFrame(); + scenegraph.updateNextFrame(); }); video.addEventListener('error', handleVideoLoadFail); video.addEventListener('abort', handleVideoLoadFail); @@ -447,6 +451,10 @@ export function createVideoCellGroup( image.textBaseline = textBaseline; cellGroup.appendChild(image); image.successCallback = () => { + const scenegraph = table.scenegraph; + if (!scenegraph) { + return; + } //补丁处理,上面loadeddata已经有一些尺寸处理,对应image-cell中updateAutoSizingAndKeepAspectRatio处理, //image重新赋值为损坏的图片的资源地址后,successCallback回调处理 //仿照image-cell.ts中的处理方法updateAutoSizingAndKeepAspectRatio @@ -461,7 +469,7 @@ export function createVideoCellGroup( cellGroup, table ); - table.scenegraph.updateNextFrame(); + scenegraph.updateNextFrame(); } }; diff --git a/packages/vtable/src/scenegraph/group-creater/column-helper.ts b/packages/vtable/src/scenegraph/group-creater/column-helper.ts index 772d5b31ae..9896dd5fc1 100644 --- a/packages/vtable/src/scenegraph/group-creater/column-helper.ts +++ b/packages/vtable/src/scenegraph/group-creater/column-helper.ts @@ -169,12 +169,12 @@ export function createComplexColumn( } } let isVtableMerge = false; - if (table.internalProps.enableTreeNodeMerge && isMerge) { + if (table.internalProps.enableTreeNodeMerge && range) { const rawRecord = table.getCellRawRecord(range.start.col, range.start.row); const { vtableMergeName, vtableMerge } = rawRecord ?? {}; - isVtableMerge = vtableMerge; - if (vtableMerge) { + isVtableMerge = vtableMerge && (isMerge || !table.isSeriesNumberInBody(col, row)); + if (isVtableMerge) { mayHaveIcon = true; if ((table.internalProps as ListTableProtected).groupTitleCustomLayout) { customResult = dealWithCustom( diff --git a/packages/vtable/src/scenegraph/runtime-contributions.ts b/packages/vtable/src/scenegraph/runtime-contributions.ts new file mode 100644 index 0000000000..9fe3a481b0 --- /dev/null +++ b/packages/vtable/src/scenegraph/runtime-contributions.ts @@ -0,0 +1,18 @@ +import type { IApp } from '@visactor/vrender-core'; +import { installRuntimeContributionModule } from '@visactor/vrender/entries/runtime-contribution'; +import { CanvasPickerContribution } from '@visactor/vrender-kits/picker/contributions/constants'; + +import splitModule from './graphic/contributions'; +import textMeasureModule from './utils/text-measure'; + +export function installVTableRuntimeContributions(app?: IApp): void { + installRuntimeContributionModule(splitModule, { + app, + targets: ['graphic-renderer', 'draw-contribution', { picker: CanvasPickerContribution }] + }); + + installRuntimeContributionModule(textMeasureModule, { + app, + targets: ['graphic-renderer'] + }); +} diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index 58737fd8ae..0330409fa0 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -1,5 +1,20 @@ -import type { IStage, IRect, ITextCache, INode, Text, RichText, Stage, IRectGraphicAttribute } from '@src/vrender'; -import { createStage, createRect, IContainPointMode, container, vglobal, registerForVrender } from '@src/vrender'; +import { + createRect, + type CheckBox, + type FederatedPointerEvent, + type IContainPointMode, + type INode, + type IRect, + type IRectGraphicAttribute, + type IStage, + type ITextCache, + type RichText, + type Stage, + type Text, + setPoptipTheme, + registerForVrender +} from '@src/vrender'; +import { createStageFromVRenderApp } from '../vrender-app'; import type { CellRange, CellSubLocation, PivotChartConstructorOptions } from '../ts-types'; import { type CellAddress, @@ -19,7 +34,6 @@ import { updateRowHeight } from './layout/update-height'; import { updateImageCellContentWhileResize } from './group-creater/cell-type/image-cell'; import { getQuadProps } from './utils/padding'; import { createFrameBorder, updateCornerRadius, updateFrameBorder, updateFrameBorderSize } from './style/frame-border'; -import splitModule from './graphic/contributions'; import { getFunctionalProp, getProp } from './utils/get-prop'; import { dealWithIcon } from './utils/text-icon-layout'; import { SceneProxy } from './group-creater/progress/proxy'; @@ -50,9 +64,7 @@ import { } from './refresh-node/update-chart'; import { initSceneGraph } from './group-creater/init-scenegraph'; import { updateContainerChildrenX, updateContainerChildrenY } from './utils/update-container'; -import type { CheckBox } from '@src/vrender'; -import { loadPoptip, setPoptipTheme } from '@src/vrender'; -import textMeasureModule from './utils/text-measure'; +import { installVTableRuntimeContributions } from './runtime-contributions'; import { getIconByXY, hideClickIcon, @@ -80,7 +92,6 @@ import { temporarilyUpdateSelectRectStyle } from './select/update-select-style'; import type { CheckboxContent } from './component/checkbox-content'; // import { contextModule } from './context/module'; -import type { FederatedPointerEvent } from '@src/vrender'; import { TABLE_EVENT_TYPE } from '../core/TABLE_EVENT_TYPE'; import { getCellEventArgsSet } from '../event/util'; import type { SceneEvent } from '../event/util'; @@ -92,14 +103,7 @@ import { } from './graphic/active-cell-chart-list'; registerForVrender(); - -// VChart poptip theme -// loadPoptip(); -container.load(splitModule); -container.load(textMeasureModule); -// container.load(renderServiceModule); -// container.load(contextModule); -// console.log(container); +installVTableRuntimeContributions(); export type MergeMap = Map< string, @@ -143,6 +147,8 @@ export class Scenegraph { lastSelectId: string; component: TableComponent; stage: IStage; + stageOwned: boolean = true; + releaseVRenderAppRef?: () => void; table: BaseTableAPI; isPivot: boolean; // transpose: boolean; @@ -192,45 +198,57 @@ export class Scenegraph { setPoptipTheme(this.table.theme.textPopTipStyle); let width; let height; - if (Env.mode === 'node') { - vglobal.setEnv('node', table.options.modeParams); + const mode = table.options.mode ?? (Env.mode === 'node' ? 'node' : 'browser'); + + if (mode === 'node') { width = table.canvasWidth; height = table.canvasHeight; } else if (table.options.canvas && table.options.viewBox) { - vglobal.setEnv('browser'); width = table.options.viewBox.x2 - table.options.viewBox.x1; height = table.options.viewBox.y2 - table.options.viewBox.y1; } else { - vglobal.setEnv('browser'); width = table.canvas.width; height = table.canvas.height; } - this.stage = createStage({ - canvas: table.canvas, - width, - height, - disableDirtyBounds: false, - background: table.theme.underlayBackgroundColor, - dpr: table.internalProps.pixelRatio, - enableLayout: true, - // enableHtmlAttribute: true, - // pluginList: table.isPivotChart() ? ['poptipForText'] : undefined, - beforeRender: (stage: Stage) => { - this.table.options.beforeRender && this.table.options.beforeRender(stage); - this.table.animationManager.ticker.start(); - }, - afterRender: (stage: Stage) => { - this.table.options.afterRender && this.table.options.afterRender(stage); - this.table.fireListeners('after_render', null); - // console.trace('after_render'); + const { stage, releaseAppRef, stageOwned } = createStageFromVRenderApp( + { + canvas: table.canvas, + width, + height, + disableDirtyBounds: false, + background: table.theme.underlayBackgroundColor, + dpr: table.internalProps.pixelRatio, + enableLayout: true, + // enableHtmlAttribute: true, + // pluginList: table.isPivotChart() ? ['poptipForText'] : undefined, + beforeRender: (stage: Stage) => { + this.table.options.beforeRender && this.table.options.beforeRender(stage); + this.table.animationManager.ticker.start(); + }, + afterRender: (stage: Stage) => { + this.table.options.afterRender && this.table.options.afterRender(stage); + this.table.fireListeners('after_render', null); + // console.trace('after_render'); + }, + // event: { clickInterval: 400 } + // autoRender: true + + canvasControled: !table.options.canvas, + viewBox: table.options.viewBox, + context: { appName: 'vtable' }, + ...table.options.renderOption }, - // event: { clickInterval: 400 } - // autoRender: true - - canvasControled: !table.options.canvas, - viewBox: table.options.viewBox, - ...table.options.renderOption - }); + { + mode, + scope: table.options.vRenderAppScope ?? 'vtable', + app: table.options.vRenderApp, + stage: table.options.stage, + envParams: table.options.modeParams + } + ); + this.stage = stage; + this.stageOwned = stageOwned; + this.releaseVRenderAppRef = releaseAppRef; this.stage.defaultLayer.setTheme({ group: { @@ -493,6 +511,25 @@ export class Scenegraph { this.table.reactCustomLayout?.clearCache(); } + releaseStage() { + const releaseAppRef = this.releaseVRenderAppRef; + this.releaseVRenderAppRef = undefined; + + try { + if (this.stageOwned) { + this.stage?.release(); + } else { + const tableGroup = this.tableGroup; + const tableGroupParent = tableGroup?.parent as Group | undefined; + + tableGroupParent?.removeChild?.(tableGroup); + tableGroup?.release?.(true); + } + } finally { + releaseAppRef?.(); + } + } + updateStageBackground() { this.stage.background = this.table.theme.underlayBackgroundColor; this.stage.renderNextFrame(); diff --git a/packages/vtable/src/scenegraph/utils/text-icon-layout.ts b/packages/vtable/src/scenegraph/utils/text-icon-layout.ts index 3591e1a7c5..285c09f804 100644 --- a/packages/vtable/src/scenegraph/utils/text-icon-layout.ts +++ b/packages/vtable/src/scenegraph/utils/text-icon-layout.ts @@ -19,6 +19,32 @@ import { getTargetCell } from '../../event/util'; import { TABLE_EVENT_TYPE } from '../../core/TABLE_EVENT_TYPE'; // import { createLine } from '@src/vrender'; +type CellTextMark = Text | RichText; + +export function insertTextBeforeCellIcons(cellGroup: Group, textMark: T): T { + const insertableTextMark = getInsertableTextMark(cellGroup, textMark); + if (cellGroup.firstChild) { + cellGroup.insertBefore(insertableTextMark, cellGroup.firstChild); + } else { + cellGroup.appendChild(insertableTextMark); + } + return insertableTextMark; +} + +function getInsertableTextMark(cellGroup: Group, textMark: T): T { + if (textMark.isAncestorsOf(cellGroup)) { + const clonedTextMark = textMark.clone() as T; + clonedTextMark.name = textMark.name; + (clonedTextMark as any).role = (textMark as any).role; + (clonedTextMark as any).onBeforeAttributeUpdate = (textMark as any).onBeforeAttributeUpdate; + if (clonedTextMark instanceof RichText) { + clonedTextMark.bindIconEvent(); + } + return clonedTextMark; + } + return textMark; +} + /** * @description: 创建单元格内容 * cellGroup @@ -412,12 +438,7 @@ export function createCellContent( contentHeight = cellContent.AABBBounds.height(); } else { // 没有content icon,cellGroup: CellIcons + wrapText/richtext - // cellGroup.appendChild(textMark); - if (cellGroup.firstChild) { - cellGroup.insertBefore(textMark, cellGroup.firstChild); - } else { - cellGroup.appendChild(textMark); - } + textMark = insertTextBeforeCellIcons(cellGroup, textMark); contentWidth = textMark.AABBBounds.width(); contentHeight = textMark.AABBBounds.height(); } diff --git a/packages/vtable/src/scenegraph/utils/text-measure.ts b/packages/vtable/src/scenegraph/utils/text-measure.ts index 932fdc1c51..5a95bdbb6c 100644 --- a/packages/vtable/src/scenegraph/utils/text-measure.ts +++ b/packages/vtable/src/scenegraph/utils/text-measure.ts @@ -9,11 +9,6 @@ import { Text } from '@src/vrender'; import { MeasureModeEnum } from '@src/vrender'; -// eslint-disable-next-line max-len -// import { -// DefaultTextMeasureContribution, -// TextMeasureContribution -// } from '@visactor/vrender/es/core/contributions/textMeasure/textMeasure-contribution'; import type { ITextMeasureOption, ITextSize } from '@visactor/vutils'; import { isValid, TextMeasure } from '@visactor/vutils'; diff --git a/packages/vtable/src/ts-types/base-table.ts b/packages/vtable/src/ts-types/base-table.ts index 2a4e8740d1..5433438e5c 100644 --- a/packages/vtable/src/ts-types/base-table.ts +++ b/packages/vtable/src/ts-types/base-table.ts @@ -105,7 +105,7 @@ import type { EditManager } from '../edit/edit-manager'; import type { TableAnimationManager } from '../core/animation'; import type { CustomCellStylePlugin } from '../plugins/custom-cell-style'; import type { IVTablePlugin } from '../plugins/interface'; -import type { FederatedPointerEvent } from '@src/vrender'; +import type { FederatedPointerEvent, IApp, IStage } from '@src/vrender'; export interface IBaseTableProtected { element: HTMLElement; @@ -570,6 +570,10 @@ export interface BaseTableConstructorOptions { // #region for nodejs mode?: 'node' | 'browser'; + /** + * Node env params are passed to acquireSharedVRenderApp({ env: 'node', envParams: modeParams }). + * Release validation should use Node 20.19.6 or another version matching the canvas native binding ABI. + */ modeParams?: any; canvasWidth?: number | 'auto'; canvasHeight?: number | 'auto'; @@ -592,6 +596,20 @@ export interface BaseTableConstructorOptions { canvas?: HTMLCanvasElement; viewBox?: IBoundsLike; + /** + * Advanced VRender app provider. Normal VTable users do not need to pass an app. + * VTable creates and releases the stage it creates from this app, but app ownership remains with the caller. + */ + vRenderApp?: IApp; + /** + * Scope for VTable-managed shared VRender apps. Tables with the same scope, mode, and envParams identity share a ref-counted app. + */ + vRenderAppScope?: string; + /** + * Advanced borrowed VRender stage. When supplied, VTable only mounts its table group and will not release the stage or app. + * The caller owns the stage lifecycle. + */ + stage?: IStage; /** 具体同 VChart 的 Option 配置。会与表格中标准的 chart Option 配置进行合并,后在图表中使用。 */ chartOption?: any; disableInteraction?: boolean; diff --git a/packages/vtable/src/vrender-app.ts b/packages/vtable/src/vrender-app.ts new file mode 100644 index 0000000000..c9fa41e166 --- /dev/null +++ b/packages/vtable/src/vrender-app.ts @@ -0,0 +1,226 @@ +import { acquireSharedVRenderApp } from '@visactor/vrender/entries/shared'; +import { installPoptipToApp } from '@visactor/vrender-components'; +import { + installBrowserEnvToApp, + installFeishuEnvToApp, + installHarmonyEnvToApp, + installLynxEnvToApp, + installNodeEnvToApp, + installTaroEnvToApp, + installTTEnvToApp, + installWxEnvToApp +} from '@visactor/vrender-kits/installers/app'; +import { loadBrowserEnv } from '@visactor/vrender-kits/env/browser'; +import { loadFeishuEnv } from '@visactor/vrender-kits/env/feishu'; +import { loadHarmonyEnv } from '@visactor/vrender-kits/env/harmony'; +import { loadLynxEnv } from '@visactor/vrender-kits/env/lynx'; +import { loadNodeEnv } from '@visactor/vrender-kits/env/node'; +import { loadTaroEnv } from '@visactor/vrender-kits/env/taro'; +import { loadTTEnv } from '@visactor/vrender-kits/env/tt'; +import { loadWxEnv } from '@visactor/vrender-kits/env/wx'; +import type { TVRenderSharedAppEnv, TVRenderSharedAppOptions } from '@visactor/vrender/entries/shared'; +import { vglobal } from '@visactor/vrender-core'; +import type { IApp, IEnvParamsMap, IStage, IStageParams } from '@visactor/vrender-core'; +import { Env } from './tools/env'; +import { installVTableRuntimeContributions } from './scenegraph/runtime-contributions'; + +type VRenderAppEnv = TVRenderSharedAppEnv; +type VRenderStageMode = VRenderAppEnv | 'desktop-browser'; + +export type VRenderStageAppOptions = { + mode?: VRenderStageMode; + scope?: string; + app?: IApp; + stage?: IStage; + envParams?: IEnvParamsMap[VRenderAppEnv]; +}; + +export type VRenderStageAppRef = { + app?: IApp; + stage: IStage; + releaseAppRef: () => void; + stageOwned: boolean; + appOwned: boolean; +}; + +const envParamsKeyMap = new WeakMap(); +let envParamsKeyId = 0; + +const getVRenderAppEnv = (mode?: VRenderStageMode): VRenderAppEnv => { + if (mode === 'desktop-browser') { + return 'browser'; + } + + return mode ?? (Env.mode === 'node' ? 'node' : 'browser'); +}; + +const getEnvParamsKey = (envParams?: IEnvParamsMap[VRenderAppEnv]): string => { + if (envParams == null) { + return 'default'; + } + + if (typeof envParams === 'object' || typeof envParams === 'function') { + const envParamsObject = envParams as object; + const existingKey = envParamsKeyMap.get(envParamsObject); + + if (existingKey != null) { + return `object:${existingKey}`; + } + + envParamsKeyId += 1; + envParamsKeyMap.set(envParamsObject, envParamsKeyId); + return `object:${envParamsKeyId}`; + } + + return `primitive:${String(envParams)}`; +}; + +const getVRenderAppKey = (env: VRenderAppEnv, scope?: string, envParams?: IEnvParamsMap[VRenderAppEnv]): string => + `${env}:${scope ?? 'default'}:${getEnvParamsKey(envParams)}`; + +const getForcedEnvParams = ( + envParams?: IEnvParamsMap[VRenderAppEnv] +): IEnvParamsMap[VRenderAppEnv] & { force: true } => { + if (envParams != null && (typeof envParams === 'object' || typeof envParams === 'function')) { + return Object.assign(Object.create(envParams as object), { force: true }); + } + + return { force: true } as IEnvParamsMap[VRenderAppEnv] & { force: true }; +}; + +const activateLegacyVGlobalEnv = (env: VRenderAppEnv, envParams?: IEnvParamsMap[VRenderAppEnv]): void => { + switch (env) { + case 'node': + loadNodeEnv(); + break; + case 'wx': + loadWxEnv(); + break; + case 'lynx': + loadLynxEnv(); + break; + case 'harmony': + loadHarmonyEnv(); + break; + case 'taro': + loadTaroEnv(); + break; + case 'feishu': + loadFeishuEnv(); + break; + case 'tt': + loadTTEnv(); + break; + case 'browser': + default: + loadBrowserEnv(); + break; + } + + vglobal.setEnv(env, getForcedEnvParams(envParams)); +}; + +const activateSharedVRenderAppEnv = (app: IApp, env: VRenderAppEnv, envParams?: IEnvParamsMap[VRenderAppEnv]): void => { + switch (env) { + case 'node': + installNodeEnvToApp(app, envParams as IEnvParamsMap['node']); + break; + case 'wx': + installWxEnvToApp(app, envParams as IEnvParamsMap['wx']); + break; + case 'lynx': + installLynxEnvToApp(app, envParams as IEnvParamsMap['lynx']); + break; + case 'harmony': + installHarmonyEnvToApp(app, envParams as IEnvParamsMap['harmony']); + break; + case 'taro': + installTaroEnvToApp(app, envParams as IEnvParamsMap['taro']); + break; + case 'feishu': + installFeishuEnvToApp(app, envParams as IEnvParamsMap['feishu']); + break; + case 'tt': + installTTEnvToApp(app, envParams as IEnvParamsMap['tt']); + break; + case 'browser': + default: + installBrowserEnvToApp(app, envParams as IEnvParamsMap['browser']); + break; + } + + activateLegacyVGlobalEnv(env, envParams); + installPoptipToApp(app); +}; + +const retainDefaultVRenderApp = (env: VRenderAppEnv, scope?: string, envParams?: IEnvParamsMap[VRenderAppEnv]) => { + const key = getVRenderAppKey(env, scope, envParams); + const sharedOptions = { + env, + key, + ...(envParams == null ? undefined : { envParams }) + } as TVRenderSharedAppOptions; + const handle = acquireSharedVRenderApp(sharedOptions); + let released = false; + + activateSharedVRenderAppEnv(handle.app, env, envParams); + + return { + app: handle.app, + releaseAppRef: () => { + if (released) { + return; + } + released = true; + handle.release(); + } + }; +}; + +const resolveVRenderApp = (options: VRenderStageAppOptions = {}) => { + if (options.app) { + return { + app: options.app, + appOwned: false, + releaseAppRef: (): void => undefined + }; + } + + return { + ...retainDefaultVRenderApp(getVRenderAppEnv(options.mode), options.scope, options.envParams), + appOwned: true + }; +}; + +export function createStageFromVRenderApp( + params: Partial, + options: VRenderStageAppOptions = {} +): VRenderStageAppRef { + if (options.stage) { + return { + app: options.app, + stage: options.stage, + releaseAppRef: (): void => undefined, + stageOwned: false, + appOwned: false + }; + } + + const resolvedApp = resolveVRenderApp(options); + + try { + installVTableRuntimeContributions(resolvedApp.app); + const stage = resolvedApp.app.createStage(params); + + return { + app: resolvedApp.app, + stage, + releaseAppRef: resolvedApp.releaseAppRef, + stageOwned: true, + appOwned: resolvedApp.appOwned + }; + } catch (error) { + resolvedApp.releaseAppRef(); + throw error; + } +} diff --git a/packages/vtable/src/vrender.ts b/packages/vtable/src/vrender.ts index 3b85c82eaf..610461c19d 100644 --- a/packages/vtable/src/vrender.ts +++ b/packages/vtable/src/vrender.ts @@ -1,28 +1,10 @@ import { loadPoptip } from '@visactor/vrender-components'; -import '@visactor/vrender-core'; -import { container, isBrowserEnv, isNodeEnv, preLoadAllModule, registerFlexLayoutPlugin } from '@visactor/vrender-core'; +import { getRuntimeInstallerBindingContext } from '@visactor/vrender-core/entries/runtime-installer'; import { - loadBrowserEnv, - loadNodeEnv, - registerArc, - registerArc3d, - registerArea, - registerCircle, - registerGlyph, - registerGroup, - registerImage, - registerLine, - registerPath, - registerPolygon, - registerPyramid3d, - registerRect, - registerRect3d, - registerRichtext, - registerShadowRoot, - registerSymbol, - registerText, - registerWrapText -} from '@visactor/vrender-kits'; + container as legacyContainer, + type ILegacyBindingContext, + type ServiceIdentifier +} from '@visactor/vrender-core'; // 导出版本号 // export const version = __VERSION__; @@ -32,40 +14,68 @@ export function registerForVrender() { return; } registed = true; - // 注册内置组件 - preLoadAllModule(); - - if (isBrowserEnv()) { - loadBrowserEnv(container); - } else if (isNodeEnv()) { - loadNodeEnv(container); - } - registerArc(); - // registerArc3d(); - // registerArea(); - registerCircle(); - // registerGlyph(); - registerGroup(); - registerImage(); - registerLine(); - // registerPath(); - // registerPolygon(); - // registerPyramid3d(); - registerRect(); - // registerRect3d(); - registerRichtext(); - registerShadowRoot(); - registerSymbol(); - registerText(); - registerFlexLayoutPlugin(); - // registerWrapText(); + // Default env and graphic bootstrap is owned by create*VRenderApp(). + // VTable keeps only component/custom assembly that is not covered by the app creator. loadPoptip(); +} - registerFlexLayoutPlugin(); +type LegacyBind = ILegacyBindingContext['bind']; +type LegacyRebind = ILegacyBindingContext['rebind']; +type LegacyIsBound = ILegacyBindingContext['isBound']; +type LegacyContainerModuleHandler = ( + bind: LegacyBind, + unbind: (serviceIdentifier: ServiceIdentifier) => void, + isBound: LegacyIsBound, + rebind: LegacyRebind +) => void; + +export class ContainerModule { + constructor(public readonly registry: LegacyContainerModuleHandler) {} } +const unbindLegacyService = (): void => undefined; + +export const container = Object.assign(legacyContainer, { + load(module: ContainerModule | ((context: ILegacyBindingContext) => void)): void { + const runtimeInstallerContext = getRuntimeInstallerBindingContext(); + + if (module instanceof ContainerModule) { + module.registry(legacyContainer.bind, unbindLegacyService, legacyContainer.isBound, legacyContainer.rebind); + module.registry( + runtimeInstallerContext.bind, + unbindLegacyService, + runtimeInstallerContext.isBound, + runtimeInstallerContext.rebind + ); + return; + } + + module(legacyContainer); + module(runtimeInstallerContext); + }, + get(serviceIdentifier: unknown): T { + return legacyContainer.getAll(serviceIdentifier as never)[0]; + } +}); + +export const injectable = + () => + (target: T): T => + target; + +export const inject = + (_serviceIdentifier: unknown) => + (..._args: unknown[]): void => + undefined; + +export const named = + (_name: unknown) => + (..._args: unknown[]): void => + undefined; + export type { Direction } from '@visactor/vrender-core'; export type { State } from '@visactor/vrender-components'; +export type { VRenderStageAppOptions, VRenderStageAppRef } from './vrender-app'; // export { GroupFadeIn } from '@visactor/vrender-core'; // export { GroupFadeOut } from '@visactor/vrender-core'; diff --git a/packages/vue-vtable/jest.config.js b/packages/vue-vtable/jest.config.js index a9e269b8b2..bfdfee91b2 100644 --- a/packages/vue-vtable/jest.config.js +++ b/packages/vue-vtable/jest.config.js @@ -1,4 +1,5 @@ const path = require('path'); +const { createVRenderModuleNameMapper } = require('../../common/config/jest/vrender-module-name-mapper'); module.exports = { preset: 'ts-jest', @@ -20,6 +21,7 @@ module.exports = { }, cacheDirectory: '/.jest-cache', moduleNameMapper: { + ...createVRenderModuleNameMapper('/../vtable/node_modules'), '@visactor/vtable$': '/../vtable/src/index', '@visactor/vtable/es/(.*)': '/../vtable/src/$1' }, diff --git a/packages/vue-vtable/package.json b/packages/vue-vtable/package.json index 1d4140aea0..a7c2312c8c 100644 --- a/packages/vue-vtable/package.json +++ b/packages/vue-vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vue-vtable", - "version": "1.26.4", + "version": "1.26.5", "description": "The vue version of VTable", "keywords": [ "vue", @@ -59,14 +59,14 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.4", + "@visactor/vtable": "workspace:1.26.5", "@visactor/vutils": "~1.0.17" }, "devDependencies": { "@arco-design/web-vue": "^2.11.0", "increase-memory-limit": "^1.0.7", "cross-env": "^7.0.3", - "@visactor/vchart": "2.0.13-alpha.10", + "@visactor/vchart": "2.1.3", "@internal/bundler": "workspace:*", "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 5212375b0b..e55721c4a7 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,11 +8,11 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.4", - "@visactor/vtable-gantt": "workspace:1.26.4", - "@visactor/vtable-editors": "workspace:1.26.4", - "@visactor/vtable-plugins": "workspace:1.26.4", - "@visactor/vtable-sheet": "workspace:1.26.4" + "@visactor/vtable": "workspace:1.26.5", + "@visactor/vtable-gantt": "workspace:1.26.5", + "@visactor/vtable-editors": "workspace:1.26.5", + "@visactor/vtable-plugins": "workspace:1.26.5", + "@visactor/vtable-sheet": "workspace:1.26.5" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4",