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/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..93d167eddb 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: @@ -25,8 +33,8 @@ importers: specifier: workspace:1.26.4 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 version: link:../packages/vtable @@ -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 @@ -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,18 +437,21 @@ 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 @@ -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)) @@ -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)) @@ -1242,8 +1253,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 '@visactor/vtable': specifier: workspace:1.26.4 version: link:../vtable @@ -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)) @@ -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)) @@ -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/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/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/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..d09c679b14 100644 --- a/docs/package.json +++ b/docs/package.json @@ -22,7 +22,7 @@ "@visactor/vtable-search": "workspace:1.26.4", "@visactor/vtable-sheet": "workspace:1.26.4", "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..4934de0437 100644 --- a/packages/openinula-vtable/package.json +++ b/packages/openinula-vtable/package.json @@ -50,7 +50,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/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..ba30e6b933 100644 --- a/packages/react-vtable/package.json +++ b/packages/react-vtable/package.json @@ -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-export/package.json b/packages/vtable-export/package.json index d1e692f8c1..ace1dd5c4c 100644 --- a/packages/vtable-export/package.json +++ b/packages/vtable-export/package.json @@ -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/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/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/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..678c5e3066 100644 --- a/packages/vtable-plugins/package.json +++ b/packages/vtable-plugins/package.json @@ -64,7 +64,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/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..e023fd2e10 100644 --- a/packages/vtable-search/package.json +++ b/packages/vtable-search/package.json @@ -42,7 +42,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-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/__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__/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__/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..424534f6c3 100644 --- a/packages/vtable/package.json +++ b/packages/vtable/package.json @@ -53,10 +53,11 @@ }, "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/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 cc34e4ce48..7230e26571 100644 --- a/packages/vtable/src/core/BaseTable.ts +++ b/packages/vtable/src/core/BaseTable.ts @@ -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-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/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..662913983e 100644 --- a/packages/vue-vtable/package.json +++ b/packages/vue-vtable/package.json @@ -66,7 +66,7 @@ "@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:*",