diff --git a/common/changes/@visactor/vtable/feat-react-component-container_2024-05-07-09-28.json b/common/changes/@visactor/vtable/feat-react-component-container_2024-05-07-09-28.json new file mode 100644 index 000000000..efabbdee6 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-react-component-container_2024-05-07-09-28.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "feat: add CustomComponent in react-vtable", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/feat-react-component-container_2024-05-15-06-22.json b/common/changes/@visactor/vtable/feat-react-component-container_2024-05-15-06-22.json new file mode 100644 index 000000000..da8e03133 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-react-component-container_2024-05-15-06-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "feat: add CustomLayout component in react-vtable", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 984321b4e..357ffd506 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -180,6 +180,7 @@ importers: '@types/react': ^18.0.0 '@types/react-dom': ^18.0.0 '@types/react-is': ^17.0.3 + '@types/react-reconciler': 0.28.8 '@visactor/vchart': 1.11.4 '@visactor/vtable': workspace:* '@visactor/vutils': ~0.18.9 @@ -201,6 +202,7 @@ importers: react: ^18.0.0 react-dom: ^18.0.0 react-is: ^18.2.0 + react-reconciler: 0.29.2 rimraf: 3.0.2 sass: 1.43.5 ts-jest: ^26.0.0 @@ -216,6 +218,7 @@ importers: '@visactor/vtable': link:../vtable '@visactor/vutils': 0.18.9 react-is: 18.3.1 + react-reconciler: 0.29.2_react@18.3.1 devDependencies: '@arco-design/web-react': 2.60.2_psuonouaqi5wuc37nxyknoubym '@babel/core': 7.20.12 @@ -232,6 +235,7 @@ importers: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 '@types/react-is': 17.0.7 + '@types/react-reconciler': 0.28.8 '@visactor/vchart': 1.11.4 '@vitejs/plugin-react': 3.1.0_vite@3.2.6 axios: 1.7.2 @@ -3401,6 +3405,12 @@ packages: '@types/react': 17.0.80 dev: true + /@types/react-reconciler/0.28.8: + resolution: {integrity: sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==} + dependencies: + '@types/react': 18.3.3 + dev: true + /@types/react/17.0.80: resolution: {integrity: sha512-LrgHIu2lEtIo8M7d1FcI3BdwXWoRQwMoXOZ7+dPTW0lYREjmlHl3P0U1VD0i/9tppOuv8/sam7sOjx34TxSFbA==} dependencies: @@ -11246,6 +11256,17 @@ packages: /react-is/18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + /react-reconciler/0.29.2_react@18.3.1: + resolution: {integrity: sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^18.3.1 + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + dev: false + /react-refresh/0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} diff --git a/docs/assets/demo-react/en/component/custom-component.md b/docs/assets/demo-react/en/component/custom-component.md index eea369764..746dbf023 100644 --- a/docs/assets/demo-react/en/component/custom-component.md +++ b/docs/assets/demo-react/en/component/custom-component.md @@ -12,7 +12,6 @@ link: '../guide/Developer_Ecology/react' The `CustomComponent` component facilitates overlaying external components on React-VTable components. ## Code Example - ```javascript livedemo template=vtable-react // import * as ReactVTable from '@visactor/react-vtable'; diff --git a/docs/assets/demo-react/en/custom-layout/cell-custom-component.md b/docs/assets/demo-react/en/custom-layout/cell-custom-component.md new file mode 100644 index 000000000..69744bbf5 --- /dev/null +++ b/docs/assets/demo-react/en/custom-layout/cell-custom-component.md @@ -0,0 +1,301 @@ +--- +category: examples +group: component +title: cell custom component +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/custom-cell-layout-jsx.png +order: 1-1 +link: '../guide/Developer_Ecology/react' +--- + +# cell custom component + +Like customLayout, you can use react components to customize layout. For details, please refer to [Custom Components](../guide/Developer_Ecology/react-custom-component) + +## code demo + +```javascript livedemo template=vtable-react +// import * as ReactVTable from '@visactor/react-vtable'; + +const VGroup = ReactVTable.Group; +const VText = ReactVTable.Text; +const VImage = ReactVTable.Image; +const VTag = ReactVTable.Tag; + +const records = [ + { + bloggerId: 1, + bloggerName: 'Virtual Anchor Xiaohua', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/flower.jpg', + introduction: + 'Hi everyone, I am Xiaohua, the virtual host. I am a little fairy who likes games, animation and food. I hope to share happy moments with you through live broadcast.', + fansCount: 400, + worksCount: 10, + viewCount: 5, + city: 'Dream City', + tags: ['game', 'anime', 'food'] + }, + { + bloggerId: 2, + bloggerName: 'Virtual anchor little wolf', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/wolf.jpg', + introduction: + 'Hello everyone, I am the virtual anchor Little Wolf. I like music, travel and photography, and I hope to explore the beauty of the world with you through live broadcast.', + fansCount: 800, + worksCount: 20, + viewCount: 15, + city: 'City of Music', + tags: ['music', 'travel', 'photography'] + }, + { + bloggerId: 3, + bloggerName: 'Virtual anchor bunny', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/rabbit.jpg', + introduction: + 'Hello everyone, I am the virtual anchor Xiaotu. I like painting, handicrafts and beauty makeup. I hope to share creativity and fashion with you through live broadcast.', + fansCount: 600, + worksCount: 15, + viewCount: 10, + city: 'City of Art', + tags: ['painting', 'handmade', 'beauty makeup'] + }, + { + bloggerId: 4, + bloggerName: 'Virtual anchor kitten', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/cat.jpg', + introduction: + 'Hello everyone, I am the virtual host Kitty. I am a lazy cat who likes dancing, fitness and cooking. I hope to live a healthy and happy life with everyone through the live broadcast.', + fansCount: 1000, + worksCount: 30, + viewCount: 20, + city: 'Health City', + tags: ['dance', 'fitness', 'cooking'] + }, + { + bloggerId: 5, + bloggerName: 'Virtual anchor Bear', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bear.jpg', + introduction: + 'Hello everyone, I am the virtual host Xiaoxiong. A little wise man who likes movies, reading and philosophy, I hope to explore the meaning of life with you through live broadcast.', + fansCount: 1200, + worksCount: 25, + viewCount: 18, + city: 'City of Wisdom', + tags: ['Movie', 'Literature'] + }, + { + bloggerId: 6, + bloggerName: 'Virtual anchor bird', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bird.jpeg', + introduction: + 'Hello everyone, I am the virtual anchor Xiaoniao. I like singing, acting and variety shows. I hope to be happy with everyone through the live broadcast.', + fansCount: 900, + worksCount: 12, + viewCount: 8, + city: 'Happy City', + tags: ['music', 'performance', 'variety'] + } +]; + +const CustomLayoutComponent = (props) => { + const { table, row, col, rect, text } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const record = table.getRecordByRowCol(col, row); + + const [hoverTitle, setHoverTitle] = React.useState(false); + const [hoverIcon, setHoverIcon] = React.useState(false); + const groupRef = React.useRef(null); + + return ( + + + + + + + { + setHoverTitle(true); + event.currentTarget.stage.renderNextFrame(); + }} + onMouseLeave={(event) => { + setHoverTitle(false); + event.currentTarget.stage.renderNextFrame(); + }} + > + ', + boundsPadding: [0, 0, 0, 10], + cursor: 'pointer', + background: hoverIcon ? { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } : undefined + }} + + onMouseEnter={event => { + setHoverIcon(true); + event.currentTarget.stage.renderNextFrame(); + }} + onMouseLeave={event => { + setHoverIcon(false); + event.currentTarget.stage.renderNextFrame(); + }} + > + + + + {record.tags.length + ? record.tags.map((str, i) => ( + // + + )) + : null} + + + + ); +} + +const root = ReactDom.createRoot(document.getElementById(CONTAINER_ID)); +root.render( + + + + + + + + + +); + +// release openinula instance, do not copy +window.customRelease = () => { + root.unmount(); +}; +``` diff --git a/docs/assets/demo-react/en/custom-layout/cell-custom-dom.md b/docs/assets/demo-react/en/custom-layout/cell-custom-dom.md new file mode 100644 index 000000000..4745ab3d4 --- /dev/null +++ b/docs/assets/demo-react/en/custom-layout/cell-custom-dom.md @@ -0,0 +1,204 @@ +--- +category: examples +group: component +title: cell custom dom component +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/react-vtable-cell-dom-component.jpeg +order: 1-1 +link: '../guide/Developer_Ecology/react' +--- + +# cell custom dom component + +Use ArcoDesign in cells. For details, please refer to [Custom Components](../guide/Developer_Ecology/react-custom-component) + +## code demo + +```javascript livedemo template=vtable-react +// import * as ReactVTable from '@visactor/react-vtable'; + +const { useCallback, useRef, useState } = React; +const { ListTable, ListColumn, Group } = ReactVTable; +const { + Avatar, + Comment, + Card, + Popover, + Space, + Button, + Popconfirm, + Message, + Notification + } = ArcoDesign; +const { IconHeart, IconMessage, IconStar, IconStarFill, IconHeartFill } = ArcoDesignIcon; + +const CommentComponent = (props) => { + const { table, row, col, rect, dataValue } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const record = table.getRecordByCell(col, row); + + return ( + + } + }} + > + ); +}; + +const CommentReactComponent = (props) => { + const { name } = props; + const [like, setLike] = useState(); + const [star, setStar] = useState(); + const actions = [ + , + , + + ]; + return ( + +

Here is the description of this user.

+ + } + > + {name.slice(0, 1)} + + } + content={
Comment body content.
} + datetime="1 hour" + style={{ marginTop: 10, marginLeft: 10 }} + /> + ); +}; + +const OperationComponent = (props) => { + const { table, row, col, rect, dataValue } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const record = table.getRecordByCell(col, row); + + return ( + + } + }} + > + ); +}; + +const OperationReactComponent = () => { + return ( + + + { + Message.info({ + content: 'ok' + }); + }} + onCancel={() => { + Message.error({ + content: 'cancel' + }); + }} + > + + + + ); +}; + +function generateRandomString(length) { + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; +} + +function App() { + const records = []; + for (let i = 0; i < 50; i++) { + records.push({ + id: i, + name: generateRandomString(8) + }); + } + + return ( + { + // eslint-disable-next-line no-undef + // (window as any).tableInstance = table; + }} + ReactDOM={ReactDom} + > + + + + + + + + + ); +} + +const root = ReactDom.createRoot(document.getElementById(CONTAINER_ID)); +root.render(); + +// release react instance, do not copy +window.customRelease = () => { + root.unmount(); +}; +``` diff --git a/docs/assets/demo-react/en/custom-layout/cell-custom-layout-dom.md b/docs/assets/demo-react/en/custom-layout/cell-custom-layout-dom.md new file mode 100644 index 000000000..706bed9f8 --- /dev/null +++ b/docs/assets/demo-react/en/custom-layout/cell-custom-layout-dom.md @@ -0,0 +1,249 @@ +--- +category: examples +group: component +title: cell custom component + dom component +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/react-vtable-dom-component.gif +order: 1-1 +link: '../guide/Developer_Ecology/react' +--- + +# cell custom component + dom component + +Use ArcoDesign in the cell pop-up window. For details, please refer to [Custom Components](../guide/Developer_Ecology/react-custom-component) + +## code demo + +```javascript livedemo template=vtable-react +// import * as ReactVTable from '@visactor/react-vtable'; + +const { useCallback, useRef, useState } = React; +const { ListTable, ListColumn, Group, Text, Image } = ReactVTable; +const { Avatar, Card, Space, Typography } = ArcoDesign; +const { IconThumbUp, IconShareInternal, IconMore } = ArcoDesignIcon; +const { Meta } = Card; + +const UserProfileComponent = (props) => { + const { table, row, col, rect, dataValue } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const record = table.getRecordByCell(col, row); + + const [hover, setHover] = useState(false); + + return ( + + + } + }} + onMouseEnter={(event) => { + setHover(true); + event.currentTarget.stage.renderNextFrame(); // to do: auto execute in react-vtable + }} + onMouseLeave={(event) => { + setHover(false); + event.currentTarget.stage.renderNextFrame(); + }} + > + + + + + ); +}; + +const CardInfo = (props) => { + const { bloggerName, bloggerAvatar, introduction, city } = props.record; + return props.hover ? ( + + dessert + + } + // actions={[ + // + // + // , + // + // + // , + // + // + // + // ]} + > + + {city.slice(0, 1)} + {city} + + } + title={bloggerName} + description={introduction} + /> + + ) : <>; +}; + +function App() { + const records = [ + { + bloggerId: 1, + bloggerName: 'Virtual Anchor Xiaohua', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/flower.jpg', + introduction: + 'Hi everyone, I am Xiaohua, the virtual host. I am a little fairy who likes games, animation and food. I hope to share happy moments with you through live broadcast.', + fansCount: 400, + worksCount: 10, + viewCount: 5, + city: 'Dream City', + tags: ['game', 'anime', 'food'] + }, + { + bloggerId: 2, + bloggerName: 'Virtual anchor little wolf', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/wolf.jpg', + introduction: + 'Hello everyone, I am the virtual anchor Little Wolf. I like music, travel and photography, and I hope to explore the beauty of the world with you through live broadcast.', + fansCount: 800, + worksCount: 20, + viewCount: 15, + city: 'City of Music', + tags: ['music', 'travel', 'photography'] + }, + { + bloggerId: 3, + bloggerName: 'Virtual anchor bunny', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/rabbit.jpg', + introduction: + 'Hello everyone, I am the virtual anchor Xiaotu. I like painting, handicrafts and beauty makeup. I hope to share creativity and fashion with you through live broadcast.', + fansCount: 600, + worksCount: 15, + viewCount: 10, + city: 'City of Art', + tags: ['painting', 'handmade', 'beauty makeup'] + }, + { + bloggerId: 4, + bloggerName: 'Virtual anchor kitten', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/cat.jpg', + introduction: + 'Hello everyone, I am the virtual host Kitty. I am a lazy cat who likes dancing, fitness and cooking. I hope to live a healthy and happy life with everyone through the live broadcast.', + fansCount: 1000, + worksCount: 30, + viewCount: 20, + city: 'Health City', + tags: ['dance', 'fitness', 'cooking'] + }, + { + bloggerId: 5, + bloggerName: 'Virtual anchor Bear', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bear.jpg', + introduction: + 'Hello everyone, I am the virtual host Xiaoxiong. A little wise man who likes movies, reading and philosophy, I hope to explore the meaning of life with you through live broadcast.', + fansCount: 1200, + worksCount: 25, + viewCount: 18, + city: 'City of Wisdom', + tags: ['Movie', 'Literature'] + }, + { + bloggerId: 6, + bloggerName: 'Virtual anchor bird', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bird.jpeg', + introduction: + 'Hello everyone, I am the virtual anchor Xiaoniao. I like singing, acting and variety shows. I hope to be happy with everyone through the live broadcast.', + fansCount: 900, + worksCount: 12, + viewCount: 8, + city: 'Happy City', + tags: ['music', 'performance', 'variety'] + } + ]; + + return ( + { + // eslint-disable-next-line no-undef + // (window as any).tableInstance = table; + }} + ReactDOM={ReactDom} + > + + + + + + + + + ); +} + +const root = ReactDom.createRoot(document.getElementById(CONTAINER_ID)); +root.render(); + +// release react instance, do not copy +window.customRelease = () => { + root.unmount(); +}; +``` diff --git a/docs/assets/demo-react/en/custom-layout/custom-layout.md b/docs/assets/demo-react/en/custom-layout/custom-layout.md deleted file mode 100644 index 706e3749f..000000000 --- a/docs/assets/demo-react/en/custom-layout/custom-layout.md +++ /dev/null @@ -1,297 +0,0 @@ ---- -category: examples -group: component -title: custom layout -cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/custom-cell-layout-jsx.png -order: 1-1 -link: '../guide/custom_define/custom_layout' -option: ListTable-columns-text#customLayout ---- - -# custom layout - -You can use jsx in customLayout to customize the layout. For details, please refer to [Custom Layout](../guide/custom_define/custom_layout) - -## code demo - -```javascript livedemo template=vtable-react -// import * as ReactVTable from '@visactor/react-vtable'; - -const VGroup = ReactVTable.VTable.VGroup; -const VText = ReactVTable.VTable.VText; -const VImage = ReactVTable.VTable.VImage; -const VTag = ReactVTable.VTable.VTag; - -const records = [ - { - bloggerId: 1, - bloggerName: 'Virtual Anchor Xiaohua', - bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/flower.jpg', - introduction: - 'Hi everyone, I am Xiaohua, the virtual host. I am a little fairy who likes games, animation and food. I hope to share happy moments with you through live broadcast.', - fansCount: 400, - worksCount: 10, - viewCount: 5, - city: 'Dream City', - tags: ['game', 'anime', 'food'] - }, - { - bloggerId: 2, - bloggerName: 'Virtual anchor little wolf', - bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/wolf.jpg', - introduction: - 'Hello everyone, I am the virtual anchor Little Wolf. I like music, travel and photography, and I hope to explore the beauty of the world with you through live broadcast.', - fansCount: 800, - worksCount: 20, - viewCount: 15, - city: 'City of Music', - tags: ['music', 'travel', 'photography'] - }, - { - bloggerId: 3, - bloggerName: 'Virtual anchor bunny', - bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/rabbit.jpg', - introduction: - 'Hello everyone, I am the virtual anchor Xiaotu. I like painting, handicrafts and beauty makeup. I hope to share creativity and fashion with you through live broadcast.', - fansCount: 600, - worksCount: 15, - viewCount: 10, - city: 'City of Art', - tags: ['painting', 'handmade', 'beauty makeup'] - }, - { - bloggerId: 4, - bloggerName: 'Virtual anchor kitten', - bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/cat.jpg', - introduction: - 'Hello everyone, I am the virtual host Kitty. I am a lazy cat who likes dancing, fitness and cooking. I hope to live a healthy and happy life with everyone through the live broadcast.', - fansCount: 1000, - worksCount: 30, - viewCount: 20, - city: 'Health City', - tags: ['dance', 'fitness', 'cooking'] - }, - { - bloggerId: 5, - bloggerName: 'Virtual anchor Bear', - bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bear.jpg', - introduction: - 'Hello everyone, I am the virtual host Xiaoxiong. A little wise man who likes movies, reading and philosophy, I hope to explore the meaning of life with you through live broadcast.', - fansCount: 1200, - worksCount: 25, - viewCount: 18, - city: 'City of Wisdom', - tags: ['Movie', 'Literature'] - }, - { - bloggerId: 6, - bloggerName: 'Virtual anchor bird', - bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bird.jpeg', - introduction: - 'Hello everyone, I am the virtual anchor Xiaoniao. I like singing, acting and variety shows. I hope to be happy with everyone through the live broadcast.', - fansCount: 900, - worksCount: 12, - viewCount: 8, - city: 'Happy City', - tags: ['music', 'performance', 'variety'] - } -]; - -const root = ReactDom.createRoot(document.getElementById(CONTAINER_ID)); -root.render( - - - { - const { table, row, col, rect } = args; - const { height, width } = rect || table.getCellRect(col, row); - const record = table.getRecordByRowCol(col, row); - // const jsx = jsx; - const container = ( - - - - - - - - ', - boundsPadding: [0, 0, 0, 10], - cursor: 'pointer' - }} - stateProxy={stateName => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; - } - }} - onMouseEnter={event => { - event.currentTarget.addState('hover', true, false); - event.currentTarget.stage.renderNextFrame(); - }} - onMouseLeave={event => { - event.currentTarget.removeState('hover', false); - event.currentTarget.stage.renderNextFrame(); - }} - > - - - - {record.tags.length - ? record.tags.map((str, i) => ( - // - - )) - : null} - - - - ); - - // decode(container) - return { - rootContainer: container, - renderDefault: false - }; - }} - /> - - - - -); - -// release openinula instance, do not copy -window.customRelease = () => { - root.unmount(); -}; -``` diff --git a/docs/assets/demo-react/menu.json b/docs/assets/demo-react/menu.json index 343675dec..7509032f0 100644 --- a/docs/assets/demo-react/menu.json +++ b/docs/assets/demo-react/menu.json @@ -115,10 +115,24 @@ }, "children": [ { - "path": "custom-layout", + "path": "cell-custom-component", "title": { - "zh": "自定义布局", - "en": "custom layout" + "zh": "单元格自定义组件", + "en": "cell custom component" + } + }, + { + "path": "cell-custom-layout-dom", + "title": { + "zh": "单元格自定义组件+dom组件", + "en": "cell custom component + dom component" + } + }, + { + "path": "cell-custom-dom", + "title": { + "zh": "单元格内dom组件", + "en": "cell custom dom component" } } ] diff --git a/docs/assets/demo-react/zh/custom-layout/cell-custom-component.md b/docs/assets/demo-react/zh/custom-layout/cell-custom-component.md new file mode 100644 index 000000000..f475938f7 --- /dev/null +++ b/docs/assets/demo-react/zh/custom-layout/cell-custom-component.md @@ -0,0 +1,301 @@ +--- +category: examples +group: component +title: 单元格自定义组件 +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/custom-cell-layout-jsx.png +order: 1-1 +link: '../guide/Developer_Ecology/react' +--- + +# 单元格自定义组件 + +同customLayout一样,可以使用react组件进行自定义布局,具体可以参考[自定义组件](../guide/Developer_Ecology/react-custom-component) + +## 代码演示 + +```javascript livedemo template=vtable-react +// import * as ReactVTable from '@visactor/react-vtable'; + +const VGroup = ReactVTable.Group; +const VText = ReactVTable.Text; +const VImage = ReactVTable.Image; +const VTag = ReactVTable.Tag; + +const records = [ + { + bloggerId: 1, + bloggerName: 'Virtual Anchor Xiaohua', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/flower.jpg', + introduction: + 'Hi everyone, I am Xiaohua, the virtual host. I am a little fairy who likes games, animation and food. I hope to share happy moments with you through live broadcast.', + fansCount: 400, + worksCount: 10, + viewCount: 5, + city: 'Dream City', + tags: ['game', 'anime', 'food'] + }, + { + bloggerId: 2, + bloggerName: 'Virtual anchor little wolf', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/wolf.jpg', + introduction: + 'Hello everyone, I am the virtual anchor Little Wolf. I like music, travel and photography, and I hope to explore the beauty of the world with you through live broadcast.', + fansCount: 800, + worksCount: 20, + viewCount: 15, + city: 'City of Music', + tags: ['music', 'travel', 'photography'] + }, + { + bloggerId: 3, + bloggerName: 'Virtual anchor bunny', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/rabbit.jpg', + introduction: + 'Hello everyone, I am the virtual anchor Xiaotu. I like painting, handicrafts and beauty makeup. I hope to share creativity and fashion with you through live broadcast.', + fansCount: 600, + worksCount: 15, + viewCount: 10, + city: 'City of Art', + tags: ['painting', 'handmade', 'beauty makeup'] + }, + { + bloggerId: 4, + bloggerName: 'Virtual anchor kitten', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/cat.jpg', + introduction: + 'Hello everyone, I am the virtual host Kitty. I am a lazy cat who likes dancing, fitness and cooking. I hope to live a healthy and happy life with everyone through the live broadcast.', + fansCount: 1000, + worksCount: 30, + viewCount: 20, + city: 'Health City', + tags: ['dance', 'fitness', 'cooking'] + }, + { + bloggerId: 5, + bloggerName: 'Virtual anchor Bear', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bear.jpg', + introduction: + 'Hello everyone, I am the virtual host Xiaoxiong. A little wise man who likes movies, reading and philosophy, I hope to explore the meaning of life with you through live broadcast.', + fansCount: 1200, + worksCount: 25, + viewCount: 18, + city: 'City of Wisdom', + tags: ['Movie', 'Literature'] + }, + { + bloggerId: 6, + bloggerName: 'Virtual anchor bird', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bird.jpeg', + introduction: + 'Hello everyone, I am the virtual anchor Xiaoniao. I like singing, acting and variety shows. I hope to be happy with everyone through the live broadcast.', + fansCount: 900, + worksCount: 12, + viewCount: 8, + city: 'Happy City', + tags: ['music', 'performance', 'variety'] + } +]; + +const CustomLayoutComponent = (props) => { + const { table, row, col, rect, text } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const record = table.getRecordByRowCol(col, row); + + const [hoverTitle, setHoverTitle] = React.useState(false); + const [hoverIcon, setHoverIcon] = React.useState(false); + const groupRef = React.useRef(null); + + return ( + + + + + + + { + setHoverTitle(true); + event.currentTarget.stage.renderNextFrame(); + }} + onMouseLeave={(event) => { + setHoverTitle(false); + event.currentTarget.stage.renderNextFrame(); + }} + > + ', + boundsPadding: [0, 0, 0, 10], + cursor: 'pointer', + background: hoverIcon ? { + fill: '#ccc', + cornerRadius: 5, + expandX: 1, + expandY: 1 + } : undefined + }} + + onMouseEnter={event => { + setHoverIcon(true); + event.currentTarget.stage.renderNextFrame(); + }} + onMouseLeave={event => { + setHoverIcon(false); + event.currentTarget.stage.renderNextFrame(); + }} + > + + + + {record.tags.length + ? record.tags.map((str, i) => ( + // + + )) + : null} + + + + ); +} + +const root = ReactDom.createRoot(document.getElementById(CONTAINER_ID)); +root.render( + + + + + + + + + +); + +// release openinula instance, do not copy +window.customRelease = () => { + root.unmount(); +}; +``` diff --git a/docs/assets/demo-react/zh/custom-layout/cell-custom-dom.md b/docs/assets/demo-react/zh/custom-layout/cell-custom-dom.md new file mode 100644 index 000000000..b08666947 --- /dev/null +++ b/docs/assets/demo-react/zh/custom-layout/cell-custom-dom.md @@ -0,0 +1,204 @@ +--- +category: examples +group: component +title: 单元格内dom组件 +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/react-vtable-cell-dom-component.jpeg +order: 1-1 +link: '../guide/Developer_Ecology/react' +--- + +# 单元格内dom组件 + +在单元格内使用ArcoDesign,具体可以参考[自定义组件](../guide/Developer_Ecology/react-custom-component) + +## 代码演示 + +```javascript livedemo template=vtable-react +// import * as ReactVTable from '@visactor/react-vtable'; + +const { useCallback, useRef, useState } = React; +const { ListTable, ListColumn, Group } = ReactVTable; +const { + Avatar, + Comment, + Card, + Popover, + Space, + Button, + Popconfirm, + Message, + Notification + } = ArcoDesign; +const { IconHeart, IconMessage, IconStar, IconStarFill, IconHeartFill } = ArcoDesignIcon; + +const CommentComponent = (props) => { + const { table, row, col, rect, dataValue } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const record = table.getRecordByCell(col, row); + + return ( + + } + }} + > + ); +}; + +const CommentReactComponent = (props) => { + const { name } = props; + const [like, setLike] = useState(); + const [star, setStar] = useState(); + const actions = [ + , + , + + ]; + return ( + +

Here is the description of this user.

+ + } + > + {name.slice(0, 1)} + + } + content={
Comment body content.
} + datetime="1 hour" + style={{ marginTop: 10, marginLeft: 10 }} + /> + ); +}; + +const OperationComponent = (props) => { + const { table, row, col, rect, dataValue } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const record = table.getRecordByCell(col, row); + + return ( + + } + }} + > + ); +}; + +const OperationReactComponent = () => { + return ( + + + { + Message.info({ + content: 'ok' + }); + }} + onCancel={() => { + Message.error({ + content: 'cancel' + }); + }} + > + + + + ); +}; + +function generateRandomString(length) { + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; +} + +function App() { + const records = []; + for (let i = 0; i < 50; i++) { + records.push({ + id: i, + name: generateRandomString(8) + }); + } + + return ( + { + // eslint-disable-next-line no-undef + // (window as any).tableInstance = table; + }} + ReactDOM={ReactDom} + > + + + + + + + + + ); +} + +const root = ReactDom.createRoot(document.getElementById(CONTAINER_ID)); +root.render(); + +// release react instance, do not copy +window.customRelease = () => { + root.unmount(); +}; +``` diff --git a/docs/assets/demo-react/zh/custom-layout/cell-custom-layout-dom.md b/docs/assets/demo-react/zh/custom-layout/cell-custom-layout-dom.md new file mode 100644 index 000000000..8c451c2c9 --- /dev/null +++ b/docs/assets/demo-react/zh/custom-layout/cell-custom-layout-dom.md @@ -0,0 +1,249 @@ +--- +category: examples +group: component +title: 单元格自定义组件+dom组件 +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/react-vtable-dom-component.gif +order: 1-1 +link: '../guide/Developer_Ecology/react' +--- + +# 单元格自定义组件+dom组件 + +在单元格弹窗使用ArcoDesign,具体可以参考[自定义组件](../guide/Developer_Ecology/react-custom-component) + +## 代码演示 + +```javascript livedemo template=vtable-react +// import * as ReactVTable from '@visactor/react-vtable'; + +const { useCallback, useRef, useState } = React; +const { ListTable, ListColumn, Group, Text, Image } = ReactVTable; +const { Avatar, Card, Space, Typography } = ArcoDesign; +const { IconThumbUp, IconShareInternal, IconMore } = ArcoDesignIcon; +const { Meta } = Card; + +const UserProfileComponent = (props) => { + const { table, row, col, rect, dataValue } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const record = table.getRecordByCell(col, row); + + const [hover, setHover] = useState(false); + + return ( + + + } + }} + onMouseEnter={(event) => { + setHover(true); + event.currentTarget.stage.renderNextFrame(); // to do: auto execute in react-vtable + }} + onMouseLeave={(event) => { + setHover(false); + event.currentTarget.stage.renderNextFrame(); + }} + > + + + + + ); +}; + +const CardInfo = (props) => { + const { bloggerName, bloggerAvatar, introduction, city } = props.record; + return props.hover ? ( + + dessert + + } + // actions={[ + // + // + // , + // + // + // , + // + // + // + // ]} + > + + {city.slice(0, 1)} + {city} + + } + title={bloggerName} + description={introduction} + /> + + ) : <>; +}; + +function App() { + const records = [ + { + bloggerId: 1, + bloggerName: 'Virtual Anchor Xiaohua', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/flower.jpg', + introduction: + 'Hi everyone, I am Xiaohua, the virtual host. I am a little fairy who likes games, animation and food. I hope to share happy moments with you through live broadcast.', + fansCount: 400, + worksCount: 10, + viewCount: 5, + city: 'Dream City', + tags: ['game', 'anime', 'food'] + }, + { + bloggerId: 2, + bloggerName: 'Virtual anchor little wolf', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/wolf.jpg', + introduction: + 'Hello everyone, I am the virtual anchor Little Wolf. I like music, travel and photography, and I hope to explore the beauty of the world with you through live broadcast.', + fansCount: 800, + worksCount: 20, + viewCount: 15, + city: 'City of Music', + tags: ['music', 'travel', 'photography'] + }, + { + bloggerId: 3, + bloggerName: 'Virtual anchor bunny', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/rabbit.jpg', + introduction: + 'Hello everyone, I am the virtual anchor Xiaotu. I like painting, handicrafts and beauty makeup. I hope to share creativity and fashion with you through live broadcast.', + fansCount: 600, + worksCount: 15, + viewCount: 10, + city: 'City of Art', + tags: ['painting', 'handmade', 'beauty makeup'] + }, + { + bloggerId: 4, + bloggerName: 'Virtual anchor kitten', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/cat.jpg', + introduction: + 'Hello everyone, I am the virtual host Kitty. I am a lazy cat who likes dancing, fitness and cooking. I hope to live a healthy and happy life with everyone through the live broadcast.', + fansCount: 1000, + worksCount: 30, + viewCount: 20, + city: 'Health City', + tags: ['dance', 'fitness', 'cooking'] + }, + { + bloggerId: 5, + bloggerName: 'Virtual anchor Bear', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bear.jpg', + introduction: + 'Hello everyone, I am the virtual host Xiaoxiong. A little wise man who likes movies, reading and philosophy, I hope to explore the meaning of life with you through live broadcast.', + fansCount: 1200, + worksCount: 25, + viewCount: 18, + city: 'City of Wisdom', + tags: ['Movie', 'Literature'] + }, + { + bloggerId: 6, + bloggerName: 'Virtual anchor bird', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bird.jpeg', + introduction: + 'Hello everyone, I am the virtual anchor Xiaoniao. I like singing, acting and variety shows. I hope to be happy with everyone through the live broadcast.', + fansCount: 900, + worksCount: 12, + viewCount: 8, + city: 'Happy City', + tags: ['music', 'performance', 'variety'] + } + ]; + + return ( + { + // eslint-disable-next-line no-undef + // (window as any).tableInstance = table; + }} + ReactDOM={ReactDom} + > + + + + + + + + + ); +} + +const root = ReactDom.createRoot(document.getElementById(CONTAINER_ID)); +root.render(); + +// release react instance, do not copy +window.customRelease = () => { + root.unmount(); +}; +``` diff --git a/docs/assets/demo-react/zh/custom-layout/custom-layout.md b/docs/assets/demo-react/zh/custom-layout/custom-layout.md deleted file mode 100644 index 3f44dc1d8..000000000 --- a/docs/assets/demo-react/zh/custom-layout/custom-layout.md +++ /dev/null @@ -1,297 +0,0 @@ ---- -category: examples -group: component -title: custom layout -cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/custom-cell-layout-jsx.png -order: 1-1 -link: '../guide/custom_define/custom_layout' -option: ListTable-columns-text#customLayout ---- - -# custom layout - -可以在 customLayout 中使用 jsx 进行自定义布局,具体可以参考[自定义布局](../guide/custom_define/custom_layout) - -## 代码演示 - -```javascript livedemo template=vtable-react -// import * as ReactVTable from '@visactor/react-vtable'; - -const VGroup = ReactVTable.VTable.VGroup; -const VText = ReactVTable.VTable.VText; -const VImage = ReactVTable.VTable.VImage; -const VTag = ReactVTable.VTable.VTag; - -const records = [ - { - bloggerId: 1, - bloggerName: 'Virtual Anchor Xiaohua', - bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/flower.jpg', - introduction: - 'Hi everyone, I am Xiaohua, the virtual host. I am a little fairy who likes games, animation and food. I hope to share happy moments with you through live broadcast.', - fansCount: 400, - worksCount: 10, - viewCount: 5, - city: 'Dream City', - tags: ['game', 'anime', 'food'] - }, - { - bloggerId: 2, - bloggerName: 'Virtual anchor little wolf', - bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/wolf.jpg', - introduction: - 'Hello everyone, I am the virtual anchor Little Wolf. I like music, travel and photography, and I hope to explore the beauty of the world with you through live broadcast.', - fansCount: 800, - worksCount: 20, - viewCount: 15, - city: 'City of Music', - tags: ['music', 'travel', 'photography'] - }, - { - bloggerId: 3, - bloggerName: 'Virtual anchor bunny', - bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/rabbit.jpg', - introduction: - 'Hello everyone, I am the virtual anchor Xiaotu. I like painting, handicrafts and beauty makeup. I hope to share creativity and fashion with you through live broadcast.', - fansCount: 600, - worksCount: 15, - viewCount: 10, - city: 'City of Art', - tags: ['painting', 'handmade', 'beauty makeup'] - }, - { - bloggerId: 4, - bloggerName: 'Virtual anchor kitten', - bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/cat.jpg', - introduction: - 'Hello everyone, I am the virtual host Kitty. I am a lazy cat who likes dancing, fitness and cooking. I hope to live a healthy and happy life with everyone through the live broadcast.', - fansCount: 1000, - worksCount: 30, - viewCount: 20, - city: 'Health City', - tags: ['dance', 'fitness', 'cooking'] - }, - { - bloggerId: 5, - bloggerName: 'Virtual anchor Bear', - bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bear.jpg', - introduction: - 'Hello everyone, I am the virtual host Xiaoxiong. A little wise man who likes movies, reading and philosophy, I hope to explore the meaning of life with you through live broadcast.', - fansCount: 1200, - worksCount: 25, - viewCount: 18, - city: 'City of Wisdom', - tags: ['Movie', 'Literature'] - }, - { - bloggerId: 6, - bloggerName: 'Virtual anchor bird', - bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bird.jpeg', - introduction: - 'Hello everyone, I am the virtual anchor Xiaoniao. I like singing, acting and variety shows. I hope to be happy with everyone through the live broadcast.', - fansCount: 900, - worksCount: 12, - viewCount: 8, - city: 'Happy City', - tags: ['music', 'performance', 'variety'] - } -]; - -const root = ReactDom.createRoot(document.getElementById(CONTAINER_ID)); -root.render( - - - { - const { table, row, col, rect } = args; - const { height, width } = rect || table.getCellRect(col, row); - const record = table.getRecordByRowCol(col, row); - // const jsx = jsx; - const container = ( - - - - - - - - ', - boundsPadding: [0, 0, 0, 10], - cursor: 'pointer' - }} - stateProxy={stateName => { - if (stateName === 'hover') { - return { - background: { - fill: '#ccc', - cornerRadius: 5, - expandX: 1, - expandY: 1 - } - }; - } - }} - onMouseEnter={event => { - event.currentTarget.addState('hover', true, false); - event.currentTarget.stage.renderNextFrame(); - }} - onMouseLeave={event => { - event.currentTarget.removeState('hover', false); - event.currentTarget.stage.renderNextFrame(); - }} - > - - - - {record.tags.length - ? record.tags.map((str, i) => ( - // - - )) - : null} - - - - ); - - // decode(container) - return { - rootContainer: container, - renderDefault: false - }; - }} - /> - - - - -); - -// release openinula instance, do not copy -window.customRelease = () => { - root.unmount(); -}; -``` diff --git a/docs/assets/guide/en/Developer_Ecology/react-custom-component.md b/docs/assets/guide/en/Developer_Ecology/react-custom-component.md new file mode 100644 index 000000000..45487648f --- /dev/null +++ b/docs/assets/guide/en/Developer_Ecology/react-custom-component.md @@ -0,0 +1,235 @@ +# React-VTable custom components + +## Custom cell components + +To help react developers quickly implement custom cell content, React-VTable provides the ability to encapsulate components and use them in cells. + +### Component usage + +Custom cell components are encapsulated based on [custom layout](../custom_define/custom_layout), and their usage is similar to custom layout. To use components in `ListColumn`, custom components need to pass in the `role` attribute to identify the component as a custom cell component; the `custom-layout` component will take effect in the table content part, and the `header-custom-layout` component will take effect in the table header part. There can be at most one `custom-layout` component in each column, and at most one `header-custom-layout` component. + +```tsx + + + + + + // ...... + +``` + +### Component encapsulation + +#### Default properties + +In the component, in addition to user-defined properties, like custom layouts, react-vtable also provides some default properties for components to use + +```tsx +interface CustomLayoutProps { + table: ListTable; // 表格实例 + row: number; // 行号 + col: number; // 列号 + value: FieldData; // 单元格展示数据 + dataValue: FieldData; // 单元格原始数据 + rect?: RectProps; // 单元格布局信息 +} +const CustomLayoutComponent = (props: CustomLayoutProps & UserProps) => { + const { table, row, col, rect, text } = props; + // ...... +} +``` + +#### Label + +The label returned by the component must be based on the element label provided by react-vtable (HTML tags or DOM react components cannot be used directly. If you need to use them, please refer to the next section) + +```tsx +import { Group, Text } from '@visactor/react-vtable'; + +const CustomLayoutComponent = (props: CustomLayoutFunctionArg & { text: string }) => { + const { table, row, col, rect, text } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const [hover, setHover] = useState(false); + + const fieldData = [ + { + value: 'a', + label: 'a' + }, + { + value: 'b', + label: 'b' + } + ]; + + const groupRef = useRef(null); + + return ( + + {fieldData.map(item => { + return ( + { + // eslint-disable-next-line no-console, no-undef + console.log('groupRef', groupRef.current); + setHover(true); + event.currentTarget.stage.renderNextFrame(); + }} + onMouseLeave={(event: any) => { + setHover(false); + event.currentTarget.stage.renderNextFrame(); + }} + /> + ); + })} + {hover && ( + + )} + + ); +}; +``` + +Basic primitives: + +* Text +* Rect +* Image +* Line +* ​​Arc +* Circle +* Group + +Basic components: + +* Tag +* Radio +* Checkbox Checkbox + +For specific configuration properties, please refer to [`VRender element configuration`](https://visactor.io/vrender/option/Group), and for specific usage and layout, please refer to [custom layout](../custom_define/custom_layout), [reference example](../../demo-react/component/custom-layout). + +
+ +
+ +#### Use DOM react components + +If you need to use DOM react components in components, you can specify the `react` attribute in the `attribute` property of the element component and pass the react component as the `element` property: + +```tsx + + } + }} + onMouseEnter={(event) => { + setHover(true); + event.currentTarget.stage.renderNextFrame(); + }} + onMouseLeave={(event) => { + setHover(false); + event.currentTarget.stage.renderNextFrame(); + }} +> +// ... + +``` + +
+ +
+ +The following properties are also supported in react: +* `pointerEvents` whether to respond to mouse events +* `container` Container, used to limit the component display area in the table when scrolling. If you need to limit the component display in the table content area, you need to specify it as `table.bodyDomContainer`; if you need to limit the component display in the table header area, you need to specify it as `table.headerDomContainer`; if it is a pop-up window or menu component, you do not need to configure this property +* `anchorType` Anchor type, used to specify the anchor position of the upper left corner of the component relative to the cell + * 'top' + * 'bottom' + * 'left' + * 'right' + * 'top-right' + * 'top-left' + * 'bottom-right' + * 'bottom-left' + * 'center' + +We recommend that users use the meta tags provided by react-vtable for the content displayed in the cell. For pop-ups, menus and other components triggered in the cell, you can use DOM react components. This is the best performance solution. [Reference example](../../demo-react/component/custom-layout). + +If you need to display content in a cell, use DOM react components. You need to specify `react.container` according to the restrictions on components displayed in the table content area. It should be noted that this method requires frequent updates of component-related DOM, which will have a certain impact on performance. You can refer to [custom layout](../custom_define/custom_layout). We strongly recommend that the content components in the cell use the meta tags provided by react-vtable, which is the best solution for performance. + +## Custom external components + +In order to facilitate the overlay of external components on the React-VTable component, React-VTable provides the `CustomComponent` tool component, which allows you to quickly locate external components in the table, and can be used to quickly implement functional components such as pop-ups and menus. + +```jsx + + + + + +``` + +Among them, `CustomComponent` is used as a container to position in the table and automatically match the size (based on the anchored cell). There are two specific ways to use it: + +1. Absolute positioning + +For absolute positioning, you need to specify `displayMode` as `position`, and you need to specify `x` and `y` attributes to position the container to the specified pixel position in the table (based on the upper left corner). The `width` and `height` attributes specify the pixel size of the container. + +2. Relative positioning + +For relative positioning, you need to specify `displayMode` as `cell`, the container is positioned relative to the cell, the `col` and `row` attributes are used to specify the anchored cell coordinates, the `anchor` attribute specifies the anchor position of the container relative to the cell, the `dx` and `dy` attributes specify the offset of the container relative to the anchored cell, and the `width` and `height` attributes specify the size of the container. The `dx` `dy` `width` and `height` attributes all support units of pixels or percentages. When the percentage is calculated relative to the size of the cell. + +### API + +```ts +interface CustomComponentProps { + children: React.ReactNode; + displayMode: 'position' | 'cell'; // Positioning mode + col?: number; // Anchored column coordinates + row?: number; // Anchored row coordinates + anchor?: + | 'top-left' + | 'top-center' + | 'top-right' + | 'middle-left' + | 'middle-center' + | 'middle-right' + | 'bottom-left' + | 'bottom-center' + | 'bottom-right'; // Anchored position + dx?: number | string; // x-direction offset + dy?: number | string; // y-direction offset + width?: number | string; // container width + height?: number | string; // container height +} +``` + +Custom external component demo: [custom component demo](../../demo-react/component/custom-component) \ No newline at end of file diff --git a/docs/assets/guide/en/Developer_Ecology/react.md b/docs/assets/guide/en/Developer_Ecology/react.md index 604ae2906..23c855849 100644 --- a/docs/assets/guide/en/Developer_Ecology/react.md +++ b/docs/assets/guide/en/Developer_Ecology/react.md @@ -176,7 +176,6 @@ return ( ); ``` - Grammatical label demo: [PivotTable demo](../../demo-react/grammatical-tag/pivot-table) [PivotChart demo](../../demo-react/grammatical-tag/pivot-chart) #### Components outside the table @@ -279,52 +278,3 @@ function App() { ``` For detailed description of the event, please refer to: [Event Introduction](../../guide/Event/event_list) - -## Custom component - -In order to facilitate the superposition of external components on the React-VTable component, React-VTable provides a `CustomComponent` tool component to quickly locate external components into the table. - -```jsx - - - - - -``` - -Among them, `CustomComponent` is used as a container for positioning in the table and automatically matching the size (based on anchored cells). There are two ways to use it: - -1. Absolute positioning - - For absolute positioning, you need to specify `displayMode` as `position`, `x` and `y` attributes, which are used to position the container to the specified pixel position in the table (based on the upper left corner), `width` and `height `property specifies the pixel dimensions of the container. - -2. Relative positioning - - For relative positioning, you need to specify `displayMode` as `cell`, the container is positioned relative to the cell, the `col` and `row` attributes are used to specify the anchored cell coordinates, and the `anchor` attribute specifies the container relative to the cell. The anchor position, `dx` and `dy` attributes specify the offset of the container relative to the anchor cell, and the `width` and `height` properties specify the size of the container, where `dx` `dy` `width` and The `height` attribute supports units of pixels or percentages. When it is a percentage, it is calculated relative to the size of the cell. - -### API - -```ts -interface CustomComponentProps { - children: React.ReactNode; - displayMode: 'position' | 'cell'; // positioning method - col?: number; // anchored column coordinates - row?: number; // anchored row coordinates - anchor?: - | 'top-left' - | 'top-center' - | 'top-right' - | 'middle-left' - | 'middle-center' - | 'middle-right' - | 'bottom-left' - | 'bottom-center' - | 'bottom-right'; // anchored position - dx?: number | string; // offset in x direction - dy?: number | string; // offset in y direction - width?: number | string; // container width - height?: number | string; // container height -} -``` - -[custom component demo](../../demo-react/component/custom-component) diff --git a/docs/assets/guide/menu.json b/docs/assets/guide/menu.json index c8b44d150..09a36a953 100644 --- a/docs/assets/guide/menu.json +++ b/docs/assets/guide/menu.json @@ -595,6 +595,13 @@ "en": "React-VTable" } }, + { + "path": "react-custom-component", + "title": { + "zh": "React自定义组件", + "en": "React Custom Component" + } + }, { "path": "openinula", "title": { diff --git a/docs/assets/guide/zh/Developer_Ecology/react-custom-component.md b/docs/assets/guide/zh/Developer_Ecology/react-custom-component.md new file mode 100644 index 000000000..b9fae0166 --- /dev/null +++ b/docs/assets/guide/zh/Developer_Ecology/react-custom-component.md @@ -0,0 +1,235 @@ +# React-VTable自定义组件 + +## 自定义单元格组件 + +为了方便react开发者快速实现自定义单元格内容,React-VTable 提供了封装组件并在单元格中使用的能力。 + +### 组件用法 + +自定义单元格组件在[自定义布局](../custom_define/custom_layout)的基础上封装而成,用法类似于自定义布局。在`ListColumn`中使用组件,自定义组件需要传入`role`属性,用于标识该组件为自定义单元格组件;其中`custom-layout`组件会在表格内容部分生效,`header-custom-layout`组件会在表格表头部分生效。每列中最多只能有一个`custom-layout`组件,最多只能有一个`header-custom-layout`组件。 + +```tsx + + + + + + // ...... + +``` + +### 组件封装 + +#### 默认属性 + +在组件中,除了用户定义的属性外,与自定义布局一样,react-vtable还提供了一些默认属性供组件使用 + +```tsx +interface CustomLayoutProps { + table: ListTable; // 表格实例 + row: number; // 行号 + col: number; // 列号 + value: FieldData; // 单元格展示数据 + dataValue: FieldData; // 单元格原始数据 + rect?: RectProps; // 单元格布局信息 +} +const CustomLayoutComponent = (props: CustomLayoutProps & UserProps) => { + const { table, row, col, rect, text } = props; + // ...... +} +``` + +#### 标签 + +组件返回的标签,必须是基于react-vtable提供的图元标签(不可以直接使用HTML标签或DOM react组件,如果需要使用,请参考下一节) + +```tsx +import { Group, Text } from '@visactor/react-vtable'; + +const CustomLayoutComponent = (props: CustomLayoutFunctionArg & { text: string }) => { + const { table, row, col, rect, text } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const [hover, setHover] = useState(false); + + const fieldData = [ + { + value: 'a', + label: 'a' + }, + { + value: 'b', + label: 'b' + } + ]; + + const groupRef = useRef(null); + + return ( + + {fieldData.map(item => { + return ( + { + // eslint-disable-next-line no-console, no-undef + console.log('groupRef', groupRef.current); + setHover(true); + event.currentTarget.stage.renderNextFrame(); + }} + onMouseLeave={(event: any) => { + setHover(false); + event.currentTarget.stage.renderNextFrame(); + }} + /> + ); + })} + {hover && ( + + )} + + ); +}; +``` + +基础图元: + +* Text 文字 +* Rect 矩形 +* Image 图片 +* Line 线 +* Arc 弧形 +* Circle 圆 +* Group 图元组 + +基础组件: + +* Tag 文本标签 +* Radio 单选框 +* Checkbox 复选框 + +具体配置属性可以参考[`VRender图元配置`](https://visactor.io/vrender/option/Group),具体使用和布局可以参考[自定义布局](../custom_define/custom_layout),[参考示例](../../demo-react/component/custom-layout)。 + +
+ +
+ +#### 使用DOM react组件 + +如果需要在组件中使用DOM react组件,可以在图元组件的`attribute`属性中,指定`react`属性,并将react组件作为`element`属性传入: + +```tsx + + } + }} + onMouseEnter={(event) => { + setHover(true); + event.currentTarget.stage.renderNextFrame(); + }} + onMouseLeave={(event) => { + setHover(false); + event.currentTarget.stage.renderNextFrame(); + }} +> +// ... + +``` + +
+ +
+ +react中还支持配置以下属性: +* `pointerEvents` 是否响应鼠标事件 +* `container` 容器,用于限制滚动时组件显示区域在表格中,如果需要限制组件显示在表格内容区域,需要指定为`table.bodyDomContainer`;如果需要限制组件显示在表格表头区域,需要指定为`table.headerDomContainer`;如果是弹窗或菜单类组件,不需要配置该属性 +* `anchorType` 锚定类型,用于指定组件左上角相对于单元格的锚定位置 + * 'top' + * 'bottom' + * 'left' + * 'right' + * 'top-right' + * 'top-left' + * 'bottom-right' + * 'bottom-left' + * 'center' + +我们推荐用户在单元格内展示的内容,使用react-vtable提供的图元标签,单元格内触发的弹窗、菜单等组件,可以使用DOM react组件,这样是性能最优的方案。[参考示例](../../demo-react/component/custom-layout)。 + +如果需要在单元格内展示的内容,使用DOM react组件,需要按照限制组件显示在表格内容区域,指定`react.container`。需要注意,这样的方式需要频繁更新组件相关DOM,会对性能有一定影响,可以参考[自定义布局](../custom_define/custom_layout)。我们强烈推荐将单元格内的内容组件使用react-vtable提供的图元标签,这样是性能最优的方案。 + +## 自定义外部组件 + +为了方便在 React-VTable 组件上叠加外部组件,React-VTable 提供了`CustomComponent`工具组件,方便快速将外部组件定位到表格当中,可以用来快速实现弹窗、菜单等功能组件。 + +```jsx + + + + + +``` + +其中,`CustomComponent`作为一个容器,用于在表格中定位,并自动匹配尺寸(基于锚定的单元格),具体有两种使用方式: + +1. 绝对定位 + + 绝对定位的方式,需要指定`displayMode`为`position`, 需要指定`x`和`y`属性,用于将容器定位到表格中的指定像素位置(基于左上角),`width`和`height`属性指定容器的像素尺寸。 + +2. 相对定位 + + 相对定位的方式,需要指定`displayMode`为`cell`,容器相对为单元格定位、`col`和`row`属性用于指定锚定的单元格坐标,`anchor`属性指定容器相对于单元格的锚定位置,`dx`和`dy`属性指定容器相对于锚定单元格的偏移量,`width`和`height`属性指定容器的尺寸,其中`dx` `dy` `width`和`height`属性的均支持单位为像素或百分比,为百分比时,相对于单元格的尺寸进行计算。 + +### API + +```ts +interface CustomComponentProps { + children: React.ReactNode; + displayMode: 'position' | 'cell'; // 定位方式 + col?: number; // 锚定的列坐标 + row?: number; // 锚定的行坐标 + anchor?: + | 'top-left' + | 'top-center' + | 'top-right' + | 'middle-left' + | 'middle-center' + | 'middle-right' + | 'bottom-left' + | 'bottom-center' + | 'bottom-right'; // 锚定的位置 + dx?: number | string; // x方向的偏移 + dy?: number | string; // y方向的偏移 + width?: number | string; // 容器的宽度 + height?: number | string; // 容器的高度 +} +``` + +自定义外部组件 demo:[custom component demo](../../demo-react/component/custom-component) \ No newline at end of file diff --git a/docs/assets/guide/zh/Developer_Ecology/react.md b/docs/assets/guide/zh/Developer_Ecology/react.md index bfe90fcd8..be0ab61bf 100644 --- a/docs/assets/guide/zh/Developer_Ecology/react.md +++ b/docs/assets/guide/zh/Developer_Ecology/react.md @@ -280,52 +280,3 @@ function App() { ``` 事件详细描述参考:[事件介绍](../../guide/Event/event_list) - -## 自定义外部组件 - -为了方便在 React-VTable 组件上叠加外部组件,React-VTable 提供了`CustomComponent`工具组件,方便快速将外部组件定位到表格当中。 - -```jsx - - - - - -``` - -其中,`CustomComponent`作为一个容器,用于在表格中定位,并自动匹配尺寸(基于锚定的单元格),具体有两种使用方式: - -1. 绝对定位 - - 绝对定位的方式,需要指定`displayMode`为`position`, 需要指定`x`和`y`属性,用于将容器定位到表格中的指定像素位置(基于左上角),`width`和`height`属性指定容器的像素尺寸。 - -2. 相对定位 - - 相对定位的方式,需要指定`displayMode`为`cell`,容器相对为单元格定位、`col`和`row`属性用于指定锚定的单元格坐标,`anchor`属性指定容器相对于单元格的锚定位置,`dx`和`dy`属性指定容器相对于锚定单元格的偏移量,`width`和`height`属性指定容器的尺寸,其中`dx` `dy` `width`和`height`属性的均支持单位为像素或百分比,为百分比时,相对于单元格的尺寸进行计算。 - -### API - -```ts -interface CustomComponentProps { - children: React.ReactNode; - displayMode: 'position' | 'cell'; // 定位方式 - col?: number; // 锚定的列坐标 - row?: number; // 锚定的行坐标 - anchor?: - | 'top-left' - | 'top-center' - | 'top-right' - | 'middle-left' - | 'middle-center' - | 'middle-right' - | 'bottom-left' - | 'bottom-center' - | 'bottom-right'; // 锚定的位置 - dx?: number | string; // x方向的偏移 - dy?: number | string; // y方向的偏移 - width?: number | string; // 容器的宽度 - height?: number | string; // 容器的高度 -} -``` - -自定义外部组件 demo:[custom component demo](../../demo-react/component/custom-component) diff --git a/docs/src/main.tsx b/docs/src/main.tsx index f97df93ed..a0b70518e 100644 --- a/docs/src/main.tsx +++ b/docs/src/main.tsx @@ -10,9 +10,11 @@ import * as ReactVTable from '@visactor/react-vtable'; import * as InulaVTable from '@visactor/openinula-vtable'; import { App } from './app'; import * as ArcoDesign from '@arco-design/web-react'; +import * as ArcoDesignIcon from '@arco-design/web-react/icon'; import '@arco-design/web-react/dist/css/arco.css'; (window as any).ArcoDesign = ArcoDesign; +(window as any).ArcoDesignIcon = ArcoDesignIcon; (window as any).VTable = VTable; (window as any).VTable_editors = VTableEditors; (window as any).VChart = VChart.VChart; diff --git a/packages/react-vtable/demo/src/App.tsx b/packages/react-vtable/demo/src/App.tsx index a7b638e69..bbfe21696 100644 --- a/packages/react-vtable/demo/src/App.tsx +++ b/packages/react-vtable/demo/src/App.tsx @@ -14,6 +14,10 @@ import listTableEvent from './event/list-table'; import eventRebind from './event/event-rebind'; import componentContainer from './component/component-container'; +import customLayout from './component/custom-layout'; +import customLayoutDom from './component/custom-layout-dom'; +import customLayoutDomSite from './component/custom-layout-dom-site'; +import customLayoutDomSite1 from './component/custom-layout-dom-site-1'; // export default listEditor; // export default listOptionRecord; @@ -29,4 +33,8 @@ import componentContainer from './component/component-container'; // export default listTableEvent; // export default eventRebind; -export default componentContainer; +// export default componentContainer; +export default customLayout; +// export default customLayoutDom; +// export default customLayoutDomSite; +// export default customLayoutDomSite1; diff --git a/packages/react-vtable/demo/src/component/custom-layout-dom-site-1.tsx b/packages/react-vtable/demo/src/component/custom-layout-dom-site-1.tsx new file mode 100644 index 000000000..01c20fbcd --- /dev/null +++ b/packages/react-vtable/demo/src/component/custom-layout-dom-site-1.tsx @@ -0,0 +1,185 @@ +import { useEffect, useRef, useState } from 'react'; +import ReactDOM from 'react-dom/client'; +import type { CustomLayoutFunctionArg } from '../../../src'; +import { ListTable, ListColumn, CustomLayout, Group, Text, Tag, Image } from '../../../src'; +import { + Avatar, + Comment, + Card, + Popover, + Space, + Typography, + Button, + Popconfirm, + Message, + Notification +} from '@arco-design/web-react'; +import { IconHeart, IconMessage, IconStar, IconStarFill, IconHeartFill } from '@arco-design/web-react/icon'; +const { Meta } = Card; + +import '@arco-design/web-react/dist/css/arco.css'; + +const CommentComponent = (props: CustomLayoutFunctionArg) => { + const { table, row, col, rect, dataValue } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const record = table.getRecordByCell(col, row); + + return ( + + } + }} + > + ); +}; + +const CommentReactComponent = (props: { name: string }) => { + const { name } = props; + const [like, setLike] = useState(); + const [star, setStar] = useState(); + const actions = [ + , + , + + ]; + return ( + +

Here is the description of this user.

+ + } + > + {name.slice(0, 1)} + + } + content={
Comment body content.
} + datetime="1 hour" + style={{ marginTop: 10, marginLeft: 10 }} + /> + ); +}; + +const OperationComponent = (props: CustomLayoutFunctionArg) => { + const { table, row, col, rect, dataValue } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const record = table.getRecordByCell(col, row); + + return ( + + } + }} + > + ); +}; + +const OperationReactComponent = () => { + return ( + + + { + Message.info({ + content: 'ok' + }); + }} + onCancel={() => { + Message.error({ + content: 'cancel' + }); + }} + > + + + + ); +}; + +function generateRandomString(length: number) { + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; +} + +function App() { + const records = []; + for (let i = 0; i < 50; i++) { + records.push({ + id: i, + name: generateRandomString(8) + }); + } + + return ( + { + // eslint-disable-next-line no-undef + // (window as any).tableInstance = table; + }} + ReactDOM={ReactDOM} + > + + + + + + + + + ); +} + +export default App; diff --git a/packages/react-vtable/demo/src/component/custom-layout-dom-site.tsx b/packages/react-vtable/demo/src/component/custom-layout-dom-site.tsx new file mode 100644 index 000000000..7191c10e9 --- /dev/null +++ b/packages/react-vtable/demo/src/component/custom-layout-dom-site.tsx @@ -0,0 +1,227 @@ +/* eslint-disable max-len */ +import { useEffect, useRef, useState } from 'react'; +import ReactDOM from 'react-dom/client'; +import type { CustomLayoutFunctionArg } from '../../../src'; +import { ListTable, ListColumn, CustomLayout, Group, Text, Tag, Image } from '../../../src'; +import { Avatar, Button, Card, Popover, Space, Typography } from '@arco-design/web-react'; +import { IconThumbUp, IconShareInternal, IconMore } from '@arco-design/web-react/icon'; +const { Meta } = Card; + +import '@arco-design/web-react/dist/css/arco.css'; + +const UserProfileComponent = (props: CustomLayoutFunctionArg) => { + const { table, row, col, rect, dataValue } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const record = table.getRecordByCell(col, row); + + const [hover, setHover] = useState(false); + + return ( + + + } + }} + onMouseEnter={(event: any) => { + setHover(true); + event.currentTarget.stage.renderNextFrame(); // to do: auto execute in react-vtable + }} + onMouseLeave={(event: any) => { + setHover(false); + event.currentTarget.stage.renderNextFrame(); + }} + > + + + + + ); +}; + +const CardInfo = (props: { record: any; hover: boolean; row?: number }) => { + const { bloggerName, bloggerAvatar, introduction, city } = props.record; + return props.hover ? ( + + dessert + + } + actions={[ + + + , + + + , + + + + ]} + > + + {city.slice(0, 1)} + {city} + + } + title={bloggerName} + description={introduction} + /> + + ) : null; +}; + +function App() { + const records = [ + { + bloggerId: 1, + bloggerName: 'Virtual Anchor Xiaohua', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/flower.jpg', + introduction: + 'Hi everyone, I am Xiaohua, the virtual host. I am a little fairy who likes games, animation and food. I hope to share happy moments with you through live broadcast.', + fansCount: 400, + worksCount: 10, + viewCount: 5, + city: 'Dream City', + tags: ['game', 'anime', 'food'] + }, + { + bloggerId: 2, + bloggerName: 'Virtual anchor little wolf', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/wolf.jpg', + introduction: + 'Hello everyone, I am the virtual anchor Little Wolf. I like music, travel and photography, and I hope to explore the beauty of the world with you through live broadcast.', + fansCount: 800, + worksCount: 20, + viewCount: 15, + city: 'City of Music', + tags: ['music', 'travel', 'photography'] + }, + { + bloggerId: 3, + bloggerName: 'Virtual anchor bunny', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/rabbit.jpg', + introduction: + 'Hello everyone, I am the virtual anchor Xiaotu. I like painting, handicrafts and beauty makeup. I hope to share creativity and fashion with you through live broadcast.', + fansCount: 600, + worksCount: 15, + viewCount: 10, + city: 'City of Art', + tags: ['painting', 'handmade', 'beauty makeup'] + }, + { + bloggerId: 4, + bloggerName: 'Virtual anchor kitten', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/cat.jpg', + introduction: + 'Hello everyone, I am the virtual host Kitty. I am a lazy cat who likes dancing, fitness and cooking. I hope to live a healthy and happy life with everyone through the live broadcast.', + fansCount: 1000, + worksCount: 30, + viewCount: 20, + city: 'Health City', + tags: ['dance', 'fitness', 'cooking'] + }, + { + bloggerId: 5, + bloggerName: 'Virtual anchor Bear', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bear.jpg', + introduction: + 'Hello everyone, I am the virtual host Xiaoxiong. A little wise man who likes movies, reading and philosophy, I hope to explore the meaning of life with you through live broadcast.', + fansCount: 1200, + worksCount: 25, + viewCount: 18, + city: 'City of Wisdom', + tags: ['Movie', 'Literature'] + }, + { + bloggerId: 6, + bloggerName: 'Virtual anchor bird', + bloggerAvatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bird.jpeg', + introduction: + 'Hello everyone, I am the virtual anchor Xiaoniao. I like singing, acting and variety shows. I hope to be happy with everyone through the live broadcast.', + fansCount: 900, + worksCount: 12, + viewCount: 8, + city: 'Happy City', + tags: ['music', 'performance', 'variety'] + } + ]; + + return ( + { + // eslint-disable-next-line no-undef + // (window as any).tableInstance = table; + }} + ReactDOM={ReactDOM} + > + + + + + + + + + ); +} + +export default App; diff --git a/packages/react-vtable/demo/src/component/custom-layout-dom.tsx b/packages/react-vtable/demo/src/component/custom-layout-dom.tsx new file mode 100644 index 000000000..b74f63dd7 --- /dev/null +++ b/packages/react-vtable/demo/src/component/custom-layout-dom.tsx @@ -0,0 +1,314 @@ +import { useEffect, useRef, useState } from 'react'; +import ReactDOM from 'react-dom/client'; +import type { CustomLayoutFunctionArg } from '../../../src'; +import { ListTable, ListColumn, CustomLayout, Group, Text, Tag, Image } from '../../../src'; +import { Avatar, Button, Card, Popover, Space, Typography } from '@arco-design/web-react'; +import { IconThumbUp, IconShareInternal, IconMore } from '@arco-design/web-react/icon'; +const { Meta } = Card; + +import '@arco-design/web-react/dist/css/arco.css'; + +function Tooltip(props: { value: string }) { + return ( +
+ {`${props.value}(click to show more)`} +
+ ); +} + +const CustomLayoutComponent = (props: CustomLayoutFunctionArg & { text: string }) => { + const { table, row, col, rect, text } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const [hover, setHover] = useState(false); + // const row = 3; + // const width = 100; + // const height = 100; + const fieldData = [ + { + value: 'a', + label: 'a' + }, + { + value: 'b', + label: 'b' + } + ]; + + const groupRef = useRef(null); + + // useEffect(() => { + // flash(col, row, this); + // }, [hover]); + + return ( + + // } + react: { + pointerEvents: true, + element: ( + +

Here is the text content

+

Here is the text content

+ + } + > + + {text} + +
+ ) + } + }} + ref={groupRef} + > + {fieldData.map(item => { + return ( + row !== 2 && ( + { + // if (stateName === 'hover') { + // return { + // fill: 'red' + // }; + // } + // }} + onMouseEnter={(event: any) => { + // eslint-disable-next-line no-console, no-undef + console.log('groupRef', groupRef.current); + setHover(true); + event.currentTarget.stage.renderNextFrame(); // to do: auto execute in react-vtable + }} + onMouseLeave={(event: any) => { + setHover(false); + event.currentTarget.stage.renderNextFrame(); + }} + > + ) + ); + })} + {hover && ( + + )} +
+ ); +}; + +const DomCustomLayoutComponent = (props: CustomLayoutFunctionArg & { text: string }) => { + const { table, row, col, rect, text } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + + const groupRef = useRef(null); + + return ( + + } + }} + ref={groupRef} + > + ); +}; + +const UserProfileComponent = (props: CustomLayoutFunctionArg & { text: string }) => { + const { table, row, col, rect, text } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + + const [hover, setHover] = useState(false); + + // if (row === 2) { + // setHover(true); + // } + + return ( + + + } + }} + onMouseEnter={(event: any) => { + setHover(true); + event.currentTarget.stage.renderNextFrame(); // to do: auto execute in react-vtable + }} + onMouseLeave={(event: any) => { + setHover(false); + event.currentTarget.stage.renderNextFrame(); + }} + > + + + + + ); +}; + +const CardInfo = (props: { text: string; hover: boolean; row?: number }) => { + return props.hover ? ( + + dessert + + } + actions={[ + + + , + + + , + + + + ]} + > + + +

Here is the text content

+

Here is the text content

+ + } + > + A +
+ {props.text + props.row} + + } + title="Card Title" + description="This is the description" + /> +
+ ) : null; +}; + +function App() { + const records = new Array(2000).fill(['John', 18, 'male', '🏀']); + const [preStr, setPreStr] = useState('vt'); + + useEffect(() => { + // eslint-disable-next-line no-undef + setTimeout(() => { + setPreStr(preStr + '1'); + }, 1000); + }, []); + + return ( + { + // eslint-disable-next-line no-undef + (window as any).tableInstance = table; + }} + ReactDOM={ReactDOM} + > + + + + + {/* */} + {/* */} + + + + ); +} + +export default App; diff --git a/packages/react-vtable/demo/src/component/custom-layout.tsx b/packages/react-vtable/demo/src/component/custom-layout.tsx new file mode 100644 index 000000000..b055434b3 --- /dev/null +++ b/packages/react-vtable/demo/src/component/custom-layout.tsx @@ -0,0 +1,164 @@ +import { useEffect, useRef, useState } from 'react'; +import type { CustomLayoutFunctionArg } from '../../../src'; +import { ListTable, ListColumn, CustomLayout, Group, Text } from '../../../src'; + +type FieldData = { value: string; label: string }; + +const CustomLayoutComponent = (props: CustomLayoutFunctionArg & { text: string }) => { + const { table, row, col, rect, text } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const [hover, setHover] = useState(false); + // const row = 3; + // const width = 100; + // const height = 100; + const fieldData = [ + { + value: 'a', + label: 'a' + }, + { + value: 'b', + label: 'b' + } + ]; + + const groupRef = useRef(null); + + // useEffect(() => { + // flash(col, row, this); + // }, [hover]); + + return ( + + {fieldData.map(item => { + return ( + row !== 2 && ( + { + // if (stateName === 'hover') { + // return { + // fill: 'red' + // }; + // } + // }} + onMouseEnter={(event: any) => { + // eslint-disable-next-line no-console, no-undef + console.log('groupRef', groupRef.current); + setHover(true); + event.currentTarget.stage.renderNextFrame(); // to do: auto execute in react-vtable + }} + onMouseLeave={(event: any) => { + setHover(false); + event.currentTarget.stage.renderNextFrame(); + }} + > + ) + ); + })} + {hover && ( + + )} + + ); +}; + +const HeaderCustomLayoutComponent = (props: CustomLayoutFunctionArg & { text: string }) => { + const { table, row, col, rect, text } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + const { height, width } = rect || table.getCellRect(col, row); + const [hover, setHover] = useState(false); + + const groupRef = useRef(null); + + return ( + + { + // eslint-disable-next-line no-console, no-undef + console.log('groupRef-header', groupRef.current); + setHover(true); + event.currentTarget.stage.renderNextFrame(); + }} + onMouseLeave={(event: any) => { + setHover(false); + event.currentTarget.stage.renderNextFrame(); + }} + > + {hover && ( + + )} + + ); +}; + +function App() { + const records = new Array(1000).fill(['John', 18, 'male', '🏀']); + const [preStr, setPreStr] = useState('vt'); + + useEffect(() => { + // eslint-disable-next-line no-undef + setTimeout(() => { + setPreStr(preStr + '1'); + }, 1000); + }, []); + + return ( + + + + + + + + + + ); +} + +export default App; diff --git a/packages/react-vtable/demo/vite.config.ts b/packages/react-vtable/demo/vite.config.ts index f3d380d55..371704e05 100644 --- a/packages/react-vtable/demo/vite.config.ts +++ b/packages/react-vtable/demo/vite.config.ts @@ -25,6 +25,7 @@ export default defineConfig({ }, resolve: { alias: { + '@visactor/vtable/src/vrender': path.resolve(__dirname, '../../vtable/src/vrender.ts'), '@visactor/vtable': path.resolve(__dirname, '../../vtable/src/index.ts'), '@src': path.resolve(__dirname, '../../vtable/src/'), '@vutils-extension': path.resolve(__dirname, '../../vtable/src/vutil-extension-temp') diff --git a/packages/react-vtable/package.json b/packages/react-vtable/package.json index 4ef00f411..4e5564461 100644 --- a/packages/react-vtable/package.json +++ b/packages/react-vtable/package.json @@ -51,7 +51,8 @@ "dependencies": { "@visactor/vtable": "workspace:*", "@visactor/vutils": "~0.18.9", - "react-is": "^18.2.0" + "react-is": "^18.2.0", + "react-reconciler": "0.29.2" }, "devDependencies": { "@visactor/vchart": "1.11.4", @@ -97,6 +98,7 @@ "form-data": "~4.0.0", "axios": "^1.4.0", "@types/react-is": "^17.0.3", - "@arco-design/web-react": "2.60.2" + "@arco-design/web-react": "2.60.2", + "@types/react-reconciler": "0.28.8" } } \ No newline at end of file diff --git a/packages/react-vtable/src/components/base-component.tsx b/packages/react-vtable/src/components/base-component.tsx index 7d8f365d9..87cb3520d 100644 --- a/packages/react-vtable/src/components/base-component.tsx +++ b/packages/react-vtable/src/components/base-component.tsx @@ -1,3 +1,4 @@ +import type { ReactElement } from 'react'; import React, { useContext, useEffect } from 'react'; import { isEqual, isNil, pickWithout } from '@visactor/vutils'; @@ -5,12 +6,14 @@ import type { TableContextType } from '../context/table'; import RootTableContext from '../context/table'; import { bindEventsToTable } from '../eventsUtils'; import { uid } from '../util'; +import { CustomLayout } from './custom/custom-layout'; export interface BaseComponentProps { id?: string | number; + children?: React.ReactNode; } -type ComponentProps = BaseComponentProps & { updateId?: number; componentId?: number }; +type ComponentProps = BaseComponentProps & { updateId?: number; componentId?: number; componentIndex?: number }; export const createComponent = ( componentName: string, @@ -18,7 +21,7 @@ export const createComponent = ( supportedEvents?: Record | null, isSingle?: boolean ) => { - const ignoreKeys = ['id', 'updateId', 'componentId']; + const ignoreKeys = ['id', 'updateId', 'componentId', 'componentIndex', 'children']; const notOptionKeys = supportedEvents ? Object.keys(supportedEvents).concat(ignoreKeys) : ignoreKeys; const Comp: React.FC = (props: T) => { @@ -58,6 +61,15 @@ export const createComponent = ( }; }, []); + // children are all custom layout temply + // return props.children + // ? React.cloneElement(props.children as ReactElement, { componentIndex: props.componentIndex }) + // : null; + if (props.children) { + return React.Children.map(props.children as ReactElement, (child: ReactElement) => { + return React.createElement(CustomLayout, { componentIndex: props.componentIndex }, child); + }); + } return null; }; @@ -66,6 +78,22 @@ export const createComponent = ( (Comp as any).parseOption = (props: T & { updateId?: number; componentId?: string }) => { const newComponentOption: Partial = pickWithout(props, notOptionKeys); + // deal width customLayout + if (props.children) { + const { children } = props; + React.Children.map(children as ReactElement, (child: ReactElement) => { + if (child.props.role === 'custom-layout') { + (newComponentOption as any).customLayout = 'react-custom-layout'; + } + if (child.props.role === 'header-custom-layout') { + (newComponentOption as any).headerCustomLayout = 'react-custom-layout'; + } + }); + } + // if (props.children && (props.children as React.ReactElement).props.role === 'custom-layout') { + // (newComponentOption as any).customLayout = 'react-custom-layout'; + // } + return { option: newComponentOption, optionName, diff --git a/packages/react-vtable/src/components/custom/component.ts b/packages/react-vtable/src/components/custom/component.ts new file mode 100644 index 000000000..9496f6152 --- /dev/null +++ b/packages/react-vtable/src/components/custom/component.ts @@ -0,0 +1,26 @@ +import type { ReactElement, ReactNode, Ref, JSXElementConstructor } from 'react'; +import type { VRender } from '@visactor/vtable'; +type IGraphic = VRender.IGraphic; +type TagAttributes = VRender.TagAttributes; +type RadioAttributes = VRender.RadioAttributes; +type CheckboxAttributes = VRender.CheckboxAttributes; +type IEventParamsType = VRender.IEventParamsType; + +type GraphicProps = { + attribute: IGraphicGraphicAttribute; + ref?: Ref; + children?: ReactNode; +} & IEventParamsType; + +export const Tag: ( + props: GraphicProps +) => ReactElement, JSXElementConstructor>> = 'tag' as any; + +export const Radio: ( + props: GraphicProps +) => ReactElement, JSXElementConstructor>> = 'radio' as any; + +export const Checkbox: ( + props: GraphicProps +) => ReactElement, JSXElementConstructor>> = + 'checkbox' as any; diff --git a/packages/react-vtable/src/components/custom/custom-layout.tsx b/packages/react-vtable/src/components/custom/custom-layout.tsx new file mode 100644 index 000000000..f6ef5838e --- /dev/null +++ b/packages/react-vtable/src/components/custom/custom-layout.tsx @@ -0,0 +1,144 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import type { PropsWithChildren } from 'react'; +import React, { isValidElement, useCallback, useContext, useLayoutEffect, useRef } from 'react'; +import RootTableContext from '../../context/table'; +import { VRender } from '@visactor/vtable'; +import type { ICustomLayoutFuc, CustomRenderFunctionArg } from '@visactor/vtable/src/ts-types'; +import type { FiberRoot } from 'react-reconciler'; +import { reconcilor } from './reconciler'; +import { LegacyRoot } from 'react-reconciler/constants'; + +const { Group } = VRender; +type CustomLayoutProps = { componentIndex?: number }; + +export type CustomLayoutFunctionArg = Partial & { + role?: 'custom-layout' | 'header-custom-layout'; +}; + +export const CustomLayout: React.FC = (props: PropsWithChildren, ref) => { + const { componentIndex, children } = props; + if (!isValidElement(children)) { + return null; + } + const context = useContext(RootTableContext); + const { table } = context; + + const isHeaderCustomLayout = children.props.role === 'header-custom-layout'; + + // react customLayout component container cache + const container = useRef>(new Map()); + + // customLayout function for vtable + const createGraphic: ICustomLayoutFuc = useCallback( + args => { + const key = `${args.col}-${args.row}`; + let group; + if (container.current.has(key)) { + const currentContainer = container.current.get(key); + // reconcilor.updateContainer(React.cloneElement(children, { ...args }), currentContainer, null); + reconcilorUpdateContainer(children, currentContainer, group, args); + group = currentContainer.containerInfo; + } else { + group = new Group({}); + const currentContainer = reconcilor.createContainer(group, LegacyRoot, null, null, null, 'custom', null, null); + container.current.set(key, currentContainer); + reconcilorUpdateContainer(children, currentContainer, group, args); + // const ele = React.cloneElement(children, { ...args }); + // reconcilor.updateContainer(ele, currentContainer, null); + } + + return { + rootContainer: group, + renderDefault: false + }; + }, + [children] + ); + + const removeContainer = useCallback((col: number, row: number) => { + const key = `${col}-${row}`; + if (container.current.has(key)) { + const currentContainer = container.current.get(key); + reconcilor.updateContainer(null, currentContainer, null); + // group = currentContainer.containerInfo; + container.current.delete(key); + } + }, []); + + useLayoutEffect(() => { + // init and release + // eslint-disable-next-line no-undef + console.log('init', props, table); + // table && (table._reactCreateGraphic = createGraphic); // never go to here + // table?.renderWithRecreateCells(); + return () => { + // eslint-disable-next-line no-undef + console.log('release', props, table); + }; + }, []); + + useLayoutEffect(() => { + // update props + // eslint-disable-next-line no-undef + console.log('update props', props, table); + + table?.checkReactCustomLayout(); // init reactCustomLayout component + if (table && !table.reactCustomLayout?.hasReactCreateGraphic(componentIndex, isHeaderCustomLayout)) { + table.reactCustomLayout?.setReactCreateGraphic( + componentIndex, + createGraphic, + // container.current, + isHeaderCustomLayout + ); // set customLayout function + table.reactCustomLayout?.setReactRemoveGraphic(componentIndex, removeContainer, isHeaderCustomLayout); // set customLayout function + table.reactCustomLayout?.updateCustomCell(componentIndex, isHeaderCustomLayout); // update cell content + } else if (table) { + // update all container + container.current.forEach((value, key) => { + const [col, row] = key.split('-').map(Number); + const width = table.getColWidth(col); // to be fixed: may be merge cell + const height = table.getRowHeight(row); // to be fixed: may be merge cell + const currentContainer = value; + const args = { + col, + row, + dataValue: table.getCellOriginValue(col, row), + value: table.getCellValue(col, row) || '', + rect: { + left: 0, + top: 0, + right: width, + bottom: height, + width, + height + }, + table + }; + // update element in container + const group = currentContainer.containerInfo; + reconcilorUpdateContainer(children, currentContainer, group, args); + // reconcilor.updateContainer(React.cloneElement(children, { ...args }), currentContainer, null); + table.scenegraph.updateNextFrame(); + }); + } + }); + + return null; +}; + +function reconcilorUpdateContainer(children, currentContainer, group, args) { + reconcilor.updateContainer(React.cloneElement(children, { ...args }), currentContainer, null); + // group = group.firstChild; + // if (isReactElement(group.attribute.html?.dom)) { + // const div = document.createElement('div'); + // const root = ReactDOM.createRoot(div as HTMLElement); + // root.render(group.attribute.html.dom); + // group.attribute.html.dom = div; + // // debugger; + // // group.html.dom = div; + // } +} + +function isReactElement(obj) { + return obj && obj.$$typeof === Symbol.for('react.element'); +} diff --git a/packages/react-vtable/src/components/custom/graphic.ts b/packages/react-vtable/src/components/custom/graphic.ts new file mode 100644 index 000000000..b7851f97d --- /dev/null +++ b/packages/react-vtable/src/components/custom/graphic.ts @@ -0,0 +1,85 @@ +// export const Group = 'group'; +// export const Rect = 'rect'; +// export const Text = 'text'; + +import type { ReactElement, ReactNode, Ref, JSXElementConstructor } from 'react'; +import type { VRender } from '@visactor/vtable'; + +type IGraphic = VRender.IGraphic; +type IGroupGraphicAttribute = VRender.IGroupGraphicAttribute; +type ITextGraphicAttribute = VRender.ITextGraphicAttribute; +type IEventParamsType = VRender.IEventParamsType; +type IArcGraphicAttribute = VRender.IArcGraphicAttribute; +type ICircleGraphicAttribute = VRender.ICircleGraphicAttribute; +type IImageGraphicAttribute = VRender.IImageGraphicAttribute; +type ILineGraphicAttribute = VRender.ILineGraphicAttribute; +type IPathGraphicAttribute = VRender.IPathGraphicAttribute; +type IRectGraphicAttribute = VRender.IRectGraphicAttribute; +type ISymbolGraphicAttribute = VRender.ISymbolGraphicAttribute; +type IRichTextGraphicAttribute = VRender.IRichTextGraphicAttribute; +type IPolygonGraphicAttribute = VRender.IPolygonGraphicAttribute; + +type GraphicProps = { + attribute: IGraphicGraphicAttribute; + ref?: Ref; + children?: ReactNode; +} & IEventParamsType; + +export const Group: ( + props: GraphicProps +) => ReactElement, JSXElementConstructor>> = + 'group' as any; + +export const Text: ( + props: GraphicProps +) => ReactElement, JSXElementConstructor>> = + 'text' as any; + +export const Arc: ( + props: GraphicProps +) => ReactElement, JSXElementConstructor>> = + 'arc' as any; + +export const Circle: ( + props: GraphicProps +) => ReactElement, JSXElementConstructor>> = + 'circle' as any; + +export const Image: ( + props: GraphicProps +) => ReactElement, JSXElementConstructor>> = + 'image' as any; + +export const Line: ( + props: GraphicProps +) => ReactElement, JSXElementConstructor>> = + 'line' as any; + +export const Path: ( + props: GraphicProps +) => ReactElement, JSXElementConstructor>> = + 'path' as any; + +export const Rect: ( + props: GraphicProps +) => ReactElement, JSXElementConstructor>> = + 'rect' as any; + +export const Symbol: ( + props: GraphicProps +) => ReactElement, JSXElementConstructor>> = + 'symbol' as any; + +export const RichText: ( + props: GraphicProps +) => ReactElement< + GraphicProps, + JSXElementConstructor> +> = 'richtext' as any; + +export const Polygon: ( + props: GraphicProps +) => ReactElement< + GraphicProps, + JSXElementConstructor> +> = 'polygon' as any; diff --git a/packages/react-vtable/src/components/custom/reconciler.ts b/packages/react-vtable/src/components/custom/reconciler.ts new file mode 100644 index 000000000..51d2833e9 --- /dev/null +++ b/packages/react-vtable/src/components/custom/reconciler.ts @@ -0,0 +1,178 @@ +import { VRender } from '@visactor/vtable'; +import { isFunction } from '@visactor/vutils'; +import React from 'react'; +import ReactReconciler from 'react-reconciler'; +import { DefaultEventPriority } from 'react-reconciler/constants.js'; + +const { application, createText, REACT_TO_CANOPUS_EVENTS, Tag } = VRender; +type Graphic = VRender.Graphic; +type Instance = Graphic; + +export const reconcilor = ReactReconciler({ + supportsMutation: true, + supportsPersistence: false, + + createInstance: (type: string, props: any, instance) => { + const graphic = createGraphic(type, props); + if (graphic) { + bindEventsToGraphic(graphic, props); + } else { + return undefined; + // createInstance + // graphic = createGraphic('group', {}); + } + return graphic; + }, + + createTextInstance: (text, instance) => { + // const textGraphic = createText({ text }); + // return textGraphic; + // debugger; + // return document.createTextNode(text); + return undefined; + }, + + appendInitialChild: (parentInstance: Instance, childInstance: Instance) => { + parentInstance.add(childInstance); + }, + + finalizeInitialChildren: () => false, + + prepareUpdate: () => true, + + shouldSetTextContent: () => false, + + getRootHostContext: () => null, + + getChildHostContext: () => null, + + getPublicInstance: (instance: Instance) => { + return instance; + }, + + prepareForCommit: () => null, + + resetAfterCommit: () => undefined, + + preparePortalMount: () => null, + + // eslint-disable-next-line no-undef + scheduleTimeout: setTimeout, + // eslint-disable-next-line no-undef + cancelTimeout: clearTimeout, + + noTimeout: -1, + isPrimaryRenderer: false, + + getCurrentEventPriority: () => DefaultEventPriority, + + getInstanceFromNode: node => null, + + beforeActiveInstanceBlur: () => undefined, + + afterActiveInstanceBlur: () => undefined, + + prepareScopeUpdate: () => undefined, + + getInstanceFromScope: () => undefined, + + detachDeletedInstance: () => undefined, + + supportsHydration: false, + + appendChild: (parentInstance: Instance, child: Instance) => { + parentInstance.add(child); + }, + appendChildToContainer: (container: Instance, child: Instance) => { + container.add(child); + }, + + insertBefore: (parentInstance: Instance, child: Instance, beforeChild: Instance) => { + parentInstance.insertBefore(child, beforeChild); + }, + + insertInContainerBefore: (parentInstance: Instance, child: Instance, beforeChild: Instance) => { + parentInstance.insertBefore(child, beforeChild); + }, + + removeChild: (parentInstance: Instance, child: Instance) => { + child.delete(); + }, + + removeChildFromContainer: (parentInstance: Instance, child: Instance) => { + child.delete(); + }, + + commitUpdate: (instance, updatePayload, type, oldProps, newProps) => { + updateGraphicProps(instance, newProps, oldProps); + }, + + hideInstance: (instance: Instance) => { + instance.setAttribute('visible', false); + }, + + unhideInstance: (instance, props) => { + instance.setAttribute('visible', true); + }, + + clearContainer: (container: Instance) => { + container.removeAllChild(); + }, + + commitTextUpdate: (textInstance: any, oldText: string, newText: string) => { + // debugger; + } +}); + +reconcilor.injectIntoDevTools({ + // findFiberByHostInstance: () => {}, + // @ts-ignore + // eslint-disable-next-line no-undef + bundleType: process.env.NODE_ENV !== 'production' ? 1 : 0, + version: React.version, + rendererPackageName: 'react-vtable' +}); + +function createGraphic(type: string, props: any) { + // may have unwanted onxxx prop + if (type === 'tag') { + const tag = new Tag(props.attribute); + return tag; + } else if (!application.graphicService.creator[type]) { + return; + } + const graphic = application.graphicService.creator[type]((props as any).attribute); + return graphic; +} + +function isEventProp(key: string, props: any) { + return key.startsWith('on') && isFunction(props[key]); +} + +function bindEventsToGraphic(graphic: Graphic, props: any) { + for (const key in props) { + if (isEventProp(key, props)) { + graphic.addEventListener(REACT_TO_CANOPUS_EVENTS[key], props[key]); + } + } +} + +function updateGraphicProps(graphic: Graphic, newProps: any, oldProps: any) { + // deal width event update + for (const propKey in oldProps) { + if (isEventProp(propKey, oldProps) && oldProps[propKey] !== newProps[propKey]) { + graphic.removeEventListener(REACT_TO_CANOPUS_EVENTS[propKey], oldProps[propKey]); + } + } + for (const propKey in newProps) { + if (isEventProp(propKey, newProps) && oldProps[propKey] !== newProps[propKey]) { + graphic.addEventListener(REACT_TO_CANOPUS_EVENTS[propKey], newProps[propKey]); + } + } + + // update all attribute + graphic.initAttributes(newProps.attribute); + if (graphic.type === 'image') { + graphic.loadImage(newProps.attribute.image); + } +} diff --git a/packages/react-vtable/src/components/custom/vtable-browser-env-contribution.ts b/packages/react-vtable/src/components/custom/vtable-browser-env-contribution.ts new file mode 100644 index 000000000..49271ece3 --- /dev/null +++ b/packages/react-vtable/src/components/custom/vtable-browser-env-contribution.ts @@ -0,0 +1,59 @@ +import { getTargetCell, VRender } from '@visactor/vtable'; +import { isString } from '@visactor/vutils'; + +const { ContainerModule, EnvContribution, BrowserEnvContribution } = VRender; +type CreateDOMParamsType = VRender.CreateDOMParamsType; + +export const reactEnvModule = new ContainerModule((bind, unbind, isBound, rebind) => { + bind(VTableBrowserEnvContribution).toSelf().inSingletonScope(); + if (isBound(EnvContribution)) { + rebind(EnvContribution).toService(VTableBrowserEnvContribution); + } else { + bind(EnvContribution).toService(VTableBrowserEnvContribution); + } +}); + +class VTableBrowserEnvContribution extends BrowserEnvContribution { + updateDom(dom: HTMLElement, params: CreateDOMParamsType): boolean { + const tableDiv = dom.parentElement; + if (tableDiv) { + const top = parseInt(params.style.top, 10); + const left = parseInt(params.style.left, 10); + + let domWidth; + let domHeight; + if ((dom.style.display = 'none')) { + const cellGroup = getTargetCell(params.graphic); + domWidth = cellGroup.attribute.width ?? 1; + domHeight = cellGroup.attribute.height ?? 1; + } else { + domWidth = dom.offsetWidth; + domHeight = dom.offsetHeight; + } + if (top + domHeight < 0 || left + domWidth < 0 || top > tableDiv.offsetHeight || left > tableDiv.offsetWidth) { + dom.style.display = 'none'; + return false; + } + } + + const { width, height, style } = params; + + if (style) { + if (isString(style)) { + dom.setAttribute('style', style); + } else { + Object.keys(style).forEach(k => { + dom.style[k] = style[k]; + }); + } + } + if (width != null) { + dom.style.width = `${width}px`; + } + if (height != null) { + dom.style.height = `${height}px`; + } + + return true; + } +} diff --git a/packages/react-vtable/src/components/custom/vtable-react-attribute-plugin.ts b/packages/react-vtable/src/components/custom/vtable-react-attribute-plugin.ts new file mode 100644 index 000000000..69a573a5d --- /dev/null +++ b/packages/react-vtable/src/components/custom/vtable-react-attribute-plugin.ts @@ -0,0 +1,169 @@ +import { VRender } from '@visactor/vtable'; +import { calculateAnchorOfBounds, isFunction, isNil, isObject, isString, styleStringToObject } from '@visactor/vutils'; + +const { ReactAttributePlugin, application } = VRender; +type CommonDomOptions = VRender.CommonDomOptions; +type CreateDOMParamsType = VRender.CreateDOMParamsType; +type IGraphic = VRender.CreateDOMParamsType; +type IStage = VRender.CreateDOMParamsType; +type IText = VRender.CreateDOMParamsType; +type SimpleDomStyleOptions = VRender.CreateDOMParamsType; + +export class VTableReactAttributePlugin extends ReactAttributePlugin { + removeElement(id: string) { + super.removeElement(id); + delete this.htmlMap[id]; + } + + renderGraphicHTML(graphic: IGraphic) { + const { react } = graphic.attribute; + if (!react) { + return; + } + const stage = graphic.stage; + if (!stage) { + return; + } + const ReactDOM = stage.params.ReactDOM; + const { element, container } = react; + if (!(element && ReactDOM && ReactDOM.createRoot)) { + return; + } + const id = isNil(react.id) ? `${graphic.id ?? graphic._uid}_react` : react.id; + + if (this.htmlMap && this.htmlMap[id] && container && container !== this.htmlMap[id].container) { + this.removeElement(id); + } + + if (!this.htmlMap || !this.htmlMap[id]) { + // createa a wrapper contianer to be the root of react element + const { wrapContainer, nativeContainer } = this.getWrapContainer(stage, container); + + if (wrapContainer) { + const root = ReactDOM.createRoot(wrapContainer); + root.render(element); + + if (!this.htmlMap) { + this.htmlMap = {}; + } + + this.htmlMap[id] = { root, wrapContainer, nativeContainer, container, renderId: this.renderId }; + } + } else { + // update react element + this.htmlMap[id].root.render(element); + } + + if (!this.htmlMap || !this.htmlMap[id]) { + return; + } + + const { wrapContainer, nativeContainer } = this.htmlMap[id]; + + this.updateStyleOfWrapContainer(graphic, stage, wrapContainer, nativeContainer, react); + this.htmlMap[id].renderId = this.renderId; + } + + getWrapContainer(stage: IStage, userContainer?: string | HTMLElement | null, domParams?: CreateDOMParamsType) { + let nativeContainer; + if (userContainer) { + if (typeof userContainer === 'string') { + nativeContainer = application.global.getElementById(userContainer); + } else { + nativeContainer = userContainer; + } + } else { + nativeContainer = stage.window.getContainer(); + } + // 创建wrapGroup + return { + wrapContainer: application.global.createDom({ tagName: 'div', parent: nativeContainer, ...domParams }), + nativeContainer + }; + } + + updateStyleOfWrapContainer( + graphic: IGraphic, + stage: IStage, + wrapContainer: HTMLElement, + nativeContainer: HTMLElement, + options: SimpleDomStyleOptions & CommonDomOptions + ) { + const { pointerEvents } = options; + let calculateStyle = this.parseDefaultStyleFromGraphic(graphic); + + calculateStyle.display = graphic.attribute.visible !== false ? 'block' : 'none'; + // 事件穿透 + calculateStyle.pointerEvents = pointerEvents === true ? 'all' : pointerEvents ? pointerEvents : 'none'; + // 定位wrapGroup + if (!wrapContainer.style.position) { + wrapContainer.style.position = 'absolute'; + nativeContainer.style.position = 'relative'; + } + let left: number = 0; + let top: number = 0; + const b = graphic.globalAABBBounds; + + let anchorType = options.anchorType; + + if (isNil(anchorType)) { + anchorType = graphic.type === 'text' ? 'position' : 'boundsLeftTop'; + } + + if (anchorType === 'boundsLeftTop') { + // 兼容老的配置,统一配置 + anchorType = 'top-left'; + } + if (anchorType === 'position' || b.empty()) { + const matrix = graphic.globalTransMatrix; + left = matrix.e; + top = matrix.f; + } else { + const anchor = calculateAnchorOfBounds(b, anchorType); + + left = anchor.x; + top = anchor.y; + } + + // 查看wrapGroup的位置 + // const wrapGroupTL = application.global.getElementTopLeft(wrapGroup, false); + const containerTL = application.global.getElementTopLeft(nativeContainer, false); + const windowTL = stage.window.getTopLeft(false); + const offsetX = left + windowTL.left - containerTL.left; + const offsetTop = top + windowTL.top - containerTL.top; + // wrapGroup.style.transform = `translate(${offsetX}px, ${offsetTop}px)`; + calculateStyle.left = `${offsetX}px`; + calculateStyle.top = `${offsetTop}px`; + + if (graphic.type === 'text' && anchorType === 'position') { + calculateStyle = { + ...calculateStyle, + ...this.getTransformOfText(graphic as IText) + }; + } + + if (isFunction(options.style)) { + const userStyle = options.style( + { top: offsetTop, left: offsetX, width: b.width(), height: b.height() }, + graphic, + wrapContainer + ); + + if (userStyle) { + calculateStyle = { ...calculateStyle, ...userStyle }; + } + } else if (isObject(options.style)) { + calculateStyle = { ...calculateStyle, ...options.style }; + } else if (isString(options.style) && options.style) { + calculateStyle = { ...calculateStyle, ...styleStringToObject(options.style as string) }; + } + + // 更新样式 + application.global.updateDom(wrapContainer, { + width: options.width, + height: options.width, + style: calculateStyle, + graphic + }); + } +} diff --git a/packages/react-vtable/src/components/index.ts b/packages/react-vtable/src/components/index.ts index 908de112f..859f59761 100644 --- a/packages/react-vtable/src/components/index.ts +++ b/packages/react-vtable/src/components/index.ts @@ -8,6 +8,9 @@ export { Menu } from './component/menu'; export { Tooltip } from './component/tooltip'; export { CustomComponent } from './custom-component'; +export { CustomLayout, type CustomLayoutFunctionArg } from './custom/custom-layout'; +export * from './custom/graphic'; +export * from './custom/component'; type Props = { updateId?: number }; diff --git a/packages/react-vtable/src/index.ts b/packages/react-vtable/src/index.ts index 9f2eccb9f..a8295390a 100644 --- a/packages/react-vtable/src/index.ts +++ b/packages/react-vtable/src/index.ts @@ -1,7 +1,5 @@ -import * as VTable from '@visactor/vtable'; - export * from './tables'; export * from './components'; -export { VTable }; +export * from './vtable'; export const version = __VERSION__; diff --git a/packages/react-vtable/src/tables/base-table.tsx b/packages/react-vtable/src/tables/base-table.tsx index b04c08c3e..079f7c395 100644 --- a/packages/react-vtable/src/tables/base-table.tsx +++ b/packages/react-vtable/src/tables/base-table.tsx @@ -1,5 +1,6 @@ /* eslint-disable react/display-name */ -import * as VTable from '@visactor/vtable'; +// import * as VTable from '@visactor/vtable'; +import { VTable } from '../vtable'; import React, { useState, useEffect, useRef, useImperativeHandle, useCallback } from 'react'; import type { ContainerProps } from '../containers/withContainer'; import withContainer from '../containers/withContainer'; @@ -21,6 +22,9 @@ import type { // TableLifeCycleEventProps } from '../eventsUtils'; import { bindEventsToTable, TABLE_EVENTS_KEYS, TABLE_EVENTS } from '../eventsUtils'; +import { VTableReactAttributePlugin } from '../components/custom/vtable-react-attribute-plugin'; +import { reactEnvModule } from '../components/custom/vtable-browser-env-contribution'; +const { container, isBrowserEnv } = VTable.VRender; export type IVTable = VTable.ListTable | VTable.PivotTable | VTable.PivotChart; export type IOption = @@ -42,12 +46,19 @@ export interface BaseTableProps extends EventsProps { height?: number; skipFunctionDiff?: boolean; + ReactDOM?: any; + /** 表格渲染完成事件 */ onReady?: (instance: IVTable, isInitial: boolean) => void; /** throw error when chart run into an error */ onError?: (err: Error) => void; } +// for react-vtable +if (isBrowserEnv()) { + container.load(reactEnvModule); +} + type Props = React.PropsWithChildren; const notOptionKeys = [ @@ -129,7 +140,10 @@ const BaseTable: React.FC = React.forwardRef((props, ref) => { records: props.records, ...prevOption.current, ...optionFromChildren.current, - clearDOM: false + clearDOM: false, + customConfig: { + createReactContainer: true + } // ...tableContext.current?.optionFromChildren } as IOption; }, @@ -146,6 +160,10 @@ const BaseTable: React.FC = React.forwardRef((props, ref) => { } else { vtable = new VTable.ListTable(props.container, parseOption(props)); } + // vtable.scenegraph.stage.enableReactAttribute(ReactDOM); + vtable.scenegraph.stage.reactAttribute = props.ReactDOM; + vtable.scenegraph.stage.pluginService.register(new VTableReactAttributePlugin()); + vtable.scenegraph.stage.params.ReactDOM = props.ReactDOM; tableContext.current = { ...tableContext.current, table: vtable }; isUnmount.current = false; }, @@ -274,7 +292,8 @@ const BaseTable: React.FC = React.forwardRef((props, ref) => { {React.cloneElement(child as React.ReactElement>, { updateId: updateId, - componentId: childId + componentId: childId, + componentIndex: index })} ); diff --git a/packages/react-vtable/src/vtable.ts b/packages/react-vtable/src/vtable.ts new file mode 100644 index 000000000..552cc5503 --- /dev/null +++ b/packages/react-vtable/src/vtable.ts @@ -0,0 +1,3 @@ +import * as VTable from '@visactor/vtable'; + +export { VTable }; diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index ac01c2123..a069a4fd4 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -70,9 +70,9 @@ export class ListTable extends BaseTable implements ListTableAPI { internalProps.sortState = options.sortState; internalProps.dataConfig = {}; //cloneDeep(options.dataConfig ?? {}); internalProps.columns = options.columns - ? cloneDeepSpec(options.columns) + ? cloneDeepSpec(options.columns, ['children']) // children for react : options.header - ? cloneDeepSpec(options.header) + ? cloneDeepSpec(options.header, ['children']) : []; // options.columns?.forEach((colDefine, index) => { // //如果editor 是一个IEditor的实例 需要这样重新赋值 否则clone后变质了 @@ -155,7 +155,7 @@ export class ListTable extends BaseTable implements ListTableAPI { */ updateColumns(columns: ColumnsDefine) { const oldHoverState = { col: this.stateManager.hover.cellPos.col, row: this.stateManager.hover.cellPos.row }; - this.internalProps.columns = cloneDeepSpec(columns); + this.internalProps.columns = cloneDeepSpec(columns, ['children']); // columns.forEach((colDefine, index) => { // if (colDefine.editor) { // this.internalProps.columns[index].editor = colDefine.editor; @@ -397,9 +397,9 @@ export class ListTable extends BaseTable implements ListTableAPI { //更新protectedSpace this.showHeader = options.showHeader ?? true; internalProps.columns = options.columns - ? cloneDeepSpec(options.columns) + ? cloneDeepSpec(options.columns, ['children']) : options.header - ? cloneDeepSpec(options.header) + ? cloneDeepSpec(options.header, ['children']) : []; // options.columns.forEach((colDefine, index) => { // if (colDefine.editor) { diff --git a/packages/vtable/src/components/react/react-custom-layout.ts b/packages/vtable/src/components/react/react-custom-layout.ts new file mode 100644 index 000000000..4066d9092 --- /dev/null +++ b/packages/vtable/src/components/react/react-custom-layout.ts @@ -0,0 +1,99 @@ +import { Group } from '@src/vrender'; +import type { CustomRenderFunctionArg, ICustomLayoutFuc } from '../../ts-types'; +import type { BaseTableAPI } from '../../ts-types/base-table'; + +export function emptyCustomLayout(args: CustomRenderFunctionArg) { + const group = new Group({}); + return { + rootContainer: group, + renderDefault: true + }; +} + +export class ReactCustomLayout { + table: BaseTableAPI; + customLayoutFuncCache: Map; + reactRemoveGraphicCache: Map void>; + headerCustomLayoutFuncCache: Map; + headerReactRemoveGraphicCache: Map void>; + // reactContainerCache: Map>; + constructor(table: BaseTableAPI) { + this.table = table; + this.customLayoutFuncCache = new Map(); + // this.reactContainerCache = new Map(); + this.reactRemoveGraphicCache = new Map(); + this.headerCustomLayoutFuncCache = new Map(); + // this.headerCeactContainerCache = new Map(); + this.headerReactRemoveGraphicCache = new Map(); + } + + hasReactCreateGraphic(componentIndex: number, isHeaderCustomLayout?: boolean) { + if (isHeaderCustomLayout) { + return this.headerCustomLayoutFuncCache.has(componentIndex); + } + return this.customLayoutFuncCache.has(componentIndex); + } + + setReactCreateGraphic( + componentIndex: number, + createGraphic: ICustomLayoutFuc, + // containerCache: Map, + isHeaderCustomLayout?: boolean + ) { + if (isHeaderCustomLayout) { + this.headerCustomLayoutFuncCache.set(componentIndex, createGraphic); + } else { + this.customLayoutFuncCache.set(componentIndex, createGraphic); + } + // this.reactContainerCache.set(componentIndex, containerCache); + } + + setReactRemoveGraphic( + componentIndex: number, + removeGraphic: (col: number, row: number) => void, + isHeaderCustomLayout?: boolean + ) { + if (isHeaderCustomLayout) { + this.headerReactRemoveGraphicCache.set(componentIndex, removeGraphic); + } else { + this.reactRemoveGraphicCache.set(componentIndex, removeGraphic); + } + } + + updateCustomCell(componentIndex: number, isHeaderCustomLayout?: boolean) { + const table = this.table; + const col = componentIndex; + // to do: deal with transpose table + if (isHeaderCustomLayout) { + for (let row = 0; row < table.columnHeaderLevelCount; row++) { + table.scenegraph.updateCellContent(col, row); + } + } else { + for (let row = table.columnHeaderLevelCount; row < table.rowCount; row++) { + table.scenegraph.updateCellContent(col, row); + } + } + // table.scenegraph.updateNextFrame(); + table.scenegraph.renderSceneGraph(); // use sync render for faster update + } + + getCustomLayoutFunc(col: number, row: number) { + const { startInTotal } = this.table.getBodyColumnDefine(col, row) as any; + const isHeader = this.table.isHeader(col, row); + return ( + (isHeader ? this.headerCustomLayoutFuncCache.get(startInTotal) : this.customLayoutFuncCache.get(startInTotal)) || + emptyCustomLayout + ); + } + + removeCustomCell(col: number, row: number) { + const { startInTotal } = this.table.getBodyColumnDefine(col, row) as any; + const isHeader = this.table.isHeader(col, row); + const removeFun = isHeader + ? this.headerReactRemoveGraphicCache.get(startInTotal) + : this.reactRemoveGraphicCache.get(startInTotal); + if (removeFun) { + removeFun(col, row); + } + } +} diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts index fded1c8ca..5f8166725 100644 --- a/packages/vtable/src/core/BaseTable.ts +++ b/packages/vtable/src/core/BaseTable.ts @@ -128,6 +128,7 @@ import { RowSeriesNumberHelper } from './row-series-number-helper'; import { CustomCellStylePlugin, mergeStyle } from '../plugins/custom-cell-style'; import { hideCellSelectBorder, restoreCellSelectBorder } from '../scenegraph/select/update-select-border'; import type { ITextGraphicAttribute } from '@src/vrender'; +import { ReactCustomLayout } from '../components/react/react-custom-layout'; import type { ISortedMapItem } from '../data/DataSource'; import { hasAutoImageColumn } from '../layout/layout-helper'; @@ -199,6 +200,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { columnWidthComputeMode?: 'normal' | 'only-header' | 'only-body'; + reactCustomLayout?: ReactCustomLayout; _hasAutoImageColumn?: boolean; constructor(container: HTMLElement, options: BaseTableConstructorOptions = {}) { @@ -299,6 +301,15 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { internalProps.canvas = document.createElement('canvas'); internalProps.element.appendChild(internalProps.canvas); internalProps.context = internalProps.canvas.getContext('2d')!; + + if (options.customConfig?.createReactContainer) { + internalProps.bodyDomContainer = document.createElement('div'); + internalProps.bodyDomContainer.classList.add('table-component-container'); + internalProps.element.appendChild(internalProps.bodyDomContainer); + internalProps.headerDomContainer = document.createElement('div'); + internalProps.headerDomContainer.classList.add('table-component-container'); + internalProps.element.appendChild(internalProps.headerDomContainer); + } } internalProps.handler = new EventHandler(); @@ -526,7 +537,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { * 注意 这个值和options.frozenColCount 不一样!options.frozenColCount是用户实际设置的; 这里获取的值是调整过:frozen的列过宽时 frozeCount为0 */ get frozenColCount(): number { - return this.internalProps.layoutMap?.frozenColCount ?? this.internalProps.frozenColCount ?? 0; + return this.internalProps?.layoutMap?.frozenColCount ?? this.internalProps?.frozenColCount ?? 0; } /** * Set the number of frozen columns. @@ -586,7 +597,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { * Get the number of frozen rows. */ get frozenRowCount(): number { - return this.internalProps.layoutMap?.frozenRowCount ?? this.internalProps.frozenRowCount ?? 0; + return this.internalProps?.layoutMap?.frozenRowCount ?? this.internalProps?.frozenRowCount ?? 0; } /** * Set the number of frozen rows. @@ -597,7 +608,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { } get rightFrozenColCount(): number { - return this.internalProps.layoutMap?.rightFrozenColCount ?? this.internalProps.rightFrozenColCount ?? 0; + return this.internalProps?.layoutMap?.rightFrozenColCount ?? this.internalProps?.rightFrozenColCount ?? 0; } set rightFrozenColCount(rightFrozenColCount: number) { @@ -605,7 +616,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { } get bottomFrozenRowCount(): number { - return this.internalProps.layoutMap?.bottomFrozenRowCount ?? this.internalProps.bottomFrozenRowCount ?? 0; + return this.internalProps?.layoutMap?.bottomFrozenRowCount ?? this.internalProps?.bottomFrozenRowCount ?? 0; } set bottomFrozenRowCount(bottomFrozenRowCount: number) { @@ -934,6 +945,15 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { canvas.style.width = `${widthP}px`; canvas.style.height = `${heightP}px`; } + + if (this.internalProps.bodyDomContainer) { + this.internalProps.bodyDomContainer.style.width = `${widthP}px`; + this.internalProps.bodyDomContainer.style.height = `${heightP}px`; + } + if (this.internalProps.headerDomContainer) { + this.internalProps.headerDomContainer.style.width = `${widthP}px`; + this.internalProps.headerDomContainer.style.height = `${heightP}px`; + } } else if (Env.mode === 'node') { widthP = this.canvasWidth - 1; heightP = this.canvasHeight - 1; @@ -4484,4 +4504,17 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { // startInertia() { // startInertia(0, -1, 1, this.stateManager); // } + + checkReactCustomLayout() { + if (!this.reactCustomLayout) { + this.reactCustomLayout = new ReactCustomLayout(this); + } + } + + get bodyDomContainer() { + return this.internalProps.bodyDomContainer; + } + get headerDomContainer() { + return this.internalProps.headerDomContainer; + } } diff --git a/packages/vtable/src/core/style.ts b/packages/vtable/src/core/style.ts index ac546e901..545ef0e5e 100644 --- a/packages/vtable/src/core/style.ts +++ b/packages/vtable/src/core/style.ts @@ -41,6 +41,15 @@ export function importStyle() { height: 100%; text-align: left; -webkit-font-smoothing:auto; + + overflow: hidden; // for react-vtable dom custom element +} +.vtable .table-component-container { + pointer-events: none; + overflow: hidden; + position: absolute; + top: 0px; + left: 0px; } .vtable > canvas { position: absolute; diff --git a/packages/vtable/src/index.ts b/packages/vtable/src/index.ts index 5bf04dd23..9841836a2 100644 --- a/packages/vtable/src/index.ts +++ b/packages/vtable/src/index.ts @@ -45,6 +45,9 @@ import { restoreMeasureText, setCustomAlphabetCharSet } from './scenegraph/utils export { getDataCellPath } from './tools/get-data-path'; export * from './render/jsx'; +export { getTargetCell } from './event/util'; + +export * as VRender from './vrender'; export const version = __VERSION__; /** diff --git a/packages/vtable/src/scenegraph/component/custom.ts b/packages/vtable/src/scenegraph/component/custom.ts index 9eb0e3a7d..8548f837f 100644 --- a/packages/vtable/src/scenegraph/component/custom.ts +++ b/packages/vtable/src/scenegraph/component/custom.ts @@ -20,6 +20,7 @@ import type { import { Icon } from '../graphic/icon'; import type { BaseTableAPI } from '../../ts-types/base-table'; import type { percentCalcObj } from '../../render/layout'; +import { emptyCustomLayout } from '../../components/react/react-custom-layout'; import { getTargetCell } from '../../event/util'; import type { Group } from '../graphic/group'; @@ -47,6 +48,10 @@ export function dealWithCustom( let customElements; let elementsGroup: VGroup; + if (customLayout === 'react-custom-layout') { + // customLayout = table._reactCreateGraphic; + customLayout = table.reactCustomLayout?.getCustomLayoutFunc(col, row) || emptyCustomLayout; + } if (typeof customLayout === 'function') { const arg = { col, @@ -72,6 +77,8 @@ export function dealWithCustom( if (customRenderObj.rootContainer instanceof VGroup) { elementsGroup = customRenderObj.rootContainer; elementsGroup.name = CUSTOM_CONTAINER_NAME; + (elementsGroup as any).col = col; + (elementsGroup as any).row = row; // } else if (customRenderObj.rootContainer) { // customElements = customRenderObj.rootContainer.getElements(undefined, false, false); } diff --git a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts index 5d8e256d1..06a30fcdc 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts @@ -668,6 +668,14 @@ function updateCellContent( if (!addNew && (oldCellGroup.row !== row || oldCellGroup.col !== col)) { return null; } + if (!addNew && oldCellGroup.parent) { + // clear react container + if (table.reactCustomLayout) { + const reactGroup = oldCellGroup.getChildByName('custom-container'); + const { col, row } = reactGroup; + table.reactCustomLayout.removeCustomCell(col, row); + } + } const newCellGroup = createCell( type, value, @@ -691,6 +699,7 @@ function updateCellContent( customResult ); if (!addNew && oldCellGroup.parent) { + // update cell oldCellGroup.parent.insertAfter(newCellGroup, oldCellGroup); oldCellGroup.parent.removeChild(oldCellGroup); @@ -718,7 +727,7 @@ function canUseFastUpdate(col: number, row: number, oldCellGroup: Group, autoWra !autoWrapText && !autoRowHeight && !mayHaveIcon && - oldCellGroup.firstChild?.type === 'text' && + oldCellGroup.firstChild?.type === 'text' && // judgement for none text !isPromise(value) ) { return true; diff --git a/packages/vtable/src/scenegraph/layout/compute-col-width.ts b/packages/vtable/src/scenegraph/layout/compute-col-width.ts index 03cd9283c..a384b98ab 100644 --- a/packages/vtable/src/scenegraph/layout/compute-col-width.ts +++ b/packages/vtable/src/scenegraph/layout/compute-col-width.ts @@ -17,7 +17,7 @@ import type { PivotHeaderLayoutMap } from '../../layout/pivot-header-layout'; import { getAxisConfigInPivotChart } from '../../layout/chart-helper/get-axis-config'; import { computeAxisComponentWidth } from '../../components/axis/get-axis-component-size'; import { Group as VGroup } from '@src/vrender'; -import { isArray, isNumber, isObject, isString, isValid } from '@visactor/vutils'; +import { isArray, isFunction, isNumber, isObject, isValid } from '@visactor/vutils'; import { decodeReactDom, dealPercentCalc } from '../component/custom'; import { breakString } from '../utils/break-string'; @@ -456,7 +456,7 @@ function computeCustomRenderWidth(col: number, row: number, table: BaseTableAPI) rect: getCellRect(col, row, table), table }; - if (customLayout) { + if (isFunction(customLayout)) { // 处理customLayout const customLayoutObj = customLayout(arg); if (customLayoutObj.rootContainer instanceof VGroup) { diff --git a/packages/vtable/src/scenegraph/layout/compute-row-height.ts b/packages/vtable/src/scenegraph/layout/compute-row-height.ts index 1461bdedc..6ddd91d55 100644 --- a/packages/vtable/src/scenegraph/layout/compute-row-height.ts +++ b/packages/vtable/src/scenegraph/layout/compute-row-height.ts @@ -10,7 +10,7 @@ import { getQuadProps } from '../utils/padding'; import { dealWithRichTextIcon } from '../utils/text-icon-layout'; import { getAxisConfigInPivotChart } from '../../layout/chart-helper/get-axis-config'; import { computeAxisComponentHeight } from '../../components/axis/get-axis-component-size'; -import { isArray, isNumber, isObject, isString, isValid } from '@visactor/vutils'; +import { isArray, isFunction, isNumber, isObject, isValid } from '@visactor/vutils'; import { CheckBox } from '@visactor/vrender-components'; import { decodeReactDom, dealPercentCalc } from '../component/custom'; import { getCellMergeRange } from '../../tools/merge-range'; @@ -546,7 +546,7 @@ function computeCustomRenderHeight(col: number, row: number, table: BaseTableAPI rect: getCellRect(col, row, table), table }; - if (customLayout) { + if (isFunction(customLayout)) { // 处理customLayout const customLayoutObj = customLayout(arg); if (customLayoutObj.rootContainer instanceof VGroup) { diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index 0b5b85f4d..3ab8bdcaf 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -149,6 +149,7 @@ export class Scenegraph { background: table.theme.underlayBackgroundColor, dpr: table.internalProps.pixelRatio, enableLayout: true, + // enableHtmlAttribute: true, // pluginList: table.isPivotChart() ? ['poptipForText'] : undefined, afterRender: () => { this.table.fireListeners('after_render', null); @@ -1534,6 +1535,8 @@ export class Scenegraph { this.updateTableSize(); this.component.updateScrollBar(); + this.updateDomContainer(); + this.updateNextFrame(); } @@ -1931,4 +1934,18 @@ export class Scenegraph { // updateCellValue(col: number, row: number) { // updateCell(col, row, this.table); // } + updateDomContainer() { + const { headerDomContainer, bodyDomContainer } = this.table.internalProps; + if (headerDomContainer) { + headerDomContainer.style.width = `${headerDomContainer.parentElement?.offsetWidth ?? 1 - 1}px`; + headerDomContainer.style.height = `${this.table.getFrozenRowsHeight()}px`; + } + if (bodyDomContainer) { + bodyDomContainer.style.width = `${bodyDomContainer.parentElement?.offsetWidth ?? 1 - 1}px`; + bodyDomContainer.style.height = `${ + bodyDomContainer.parentElement?.offsetHeight ?? 1 - 1 - this.table.getFrozenRowsHeight() + }px`; + bodyDomContainer.style.top = `${this.table.getFrozenRowsHeight()}px`; + } + } } diff --git a/packages/vtable/src/ts-types/base-table.ts b/packages/vtable/src/ts-types/base-table.ts index 787a372e5..600ba0203 100644 --- a/packages/vtable/src/ts-types/base-table.ts +++ b/packages/vtable/src/ts-types/base-table.ts @@ -92,6 +92,7 @@ import type { DiscreteTableLegend } from '../components/legend/discrete-legend/d import type { ContinueTableLegend } from '../components/legend/continue-legend/continue-legend'; import type { NumberRangeMap } from '../layout/row-height-map'; import type { RowSeriesNumberHelper } from '../core/row-series-number-helper'; +import type { ReactCustomLayout } from '../components/react/react-custom-layout'; import type { ISortedMapItem } from '../data/DataSource'; import type { IAnimationAppear } from './animation/appear'; import type { IEmptyTip } from './component/empty-tip'; @@ -254,6 +255,9 @@ export interface IBaseTableProtected { * */ overscrollBehavior?: 'auto' | 'none'; + // react component container + bodyDomContainer?: HTMLElement; + headerDomContainer?: HTMLElement; // 已使用一行的高度填充所有行 useOneRowHeightFillAll?: boolean; } @@ -454,6 +458,8 @@ export interface BaseTableConstructorOptions { /** 禁用行高列宽计算取整数逻辑 对齐xTable */ _disableColumnAndRowSizeRound?: boolean; imageMargin?: number; + // 是否创建react custom container + createReactContainer?: boolean; // adaptive 模式下优先缩小迷你图 shrinkSparklineFirst?: boolean; }; // 部分特殊配置,兼容xTable等作用 @@ -817,6 +823,8 @@ export interface BaseTableAPI { leftRowSeriesNumberCount: number; isAutoRowHeight: (row: number) => boolean; + reactCustomLayout?: ReactCustomLayout; + checkReactCustomLayout: () => void; setSortedIndexMap: (field: FieldDef, filedMap: ISortedMapItem) => void; exportImg: () => string; @@ -828,6 +836,9 @@ export interface BaseTableAPI { exportCellRangeImg: (cellRange: CellRange) => string; exportCanvas: () => HTMLCanvasElement; setPixelRatio: (pixelRatio: number) => void; + + bodyDomContainer?: HTMLElement; + headerDomContainer?: HTMLElement; } export interface ListTableProtected extends IBaseTableProtected { /** 表格数据 */ diff --git a/packages/vtable/src/ts-types/customLayout.ts b/packages/vtable/src/ts-types/customLayout.ts index a82ab4c50..11a8d5153 100644 --- a/packages/vtable/src/ts-types/customLayout.ts +++ b/packages/vtable/src/ts-types/customLayout.ts @@ -15,4 +15,4 @@ export type ICustomLayoutObj = { export type ICustomLayoutFuc = (args: CustomRenderFunctionArg) => ICustomLayoutObj; -export type ICustomLayout = ICustomLayoutFuc; +export type ICustomLayout = ICustomLayoutFuc | 'react-custom-layout'; diff --git a/packages/vtable/src/vrender.ts b/packages/vtable/src/vrender.ts index 1827c4287..ad54c76f9 100644 --- a/packages/vtable/src/vrender.ts +++ b/packages/vtable/src/vrender.ts @@ -1,7 +1,8 @@ import '@visactor/vrender-core'; import { container, isBrowserEnv, isNodeEnv, preLoadAllModule } from '@visactor/vrender-core'; -import { loadBrowserEnv, loadNodeEnv } from '@visactor/vrender-kits'; import { + loadBrowserEnv, + loadNodeEnv, registerArc, registerArc3d, registerArea, @@ -58,5 +59,10 @@ export function registerForVrender() { registerWrapText(); } +export { Direction } from '@visactor/vrender-core'; +export { GroupFadeIn } from '@visactor/vrender-core'; +export { GroupFadeOut } from '@visactor/vrender-core'; + export * from '@visactor/vrender-core'; export * from '@visactor/vrender-kits'; +export * from '@visactor/vrender-components';