From 85d763436f29930ca281e92164b85d5a4591e4ea Mon Sep 17 00:00:00 2001 From: ajaxzheng <894103554@qq.com> Date: Tue, 17 Jun 2025 09:41:39 +0800 Subject: [PATCH] refactor: optimize table performance and refactor the table --- .../pc/app/grid/custom/column-fixed.spec.js | 3 +- .../pc/app/grid/custom/page-size.spec.js | 2 +- .../grid/data-source/request-service.spec.js | 2 +- .../app/grid/data-source/static-data.spec.js | 2 +- .../pc/app/grid/editor/custom-edit.spec.js | 9 +- .../grid/empty/empty-data-iscenter.spec.js | 2 +- .../event/toolbar-button-click-event.spec.js | 2 +- .../app/grid/expand/set-row-expansion.spec.js | 4 + .../app/grid/filter/default-relation.spec.js | 2 +- .../pc/app/grid/filter/server-filter.spec.js | 2 +- .../footer/footer-summation-empty.spec.js | 1 - .../grid/large-data/full-data-loading.spec.js | 6 +- .../keyboard-navigation.spec.js | 5 +- .../app/grid/renderer/inner-renderer.spec.js | 4 + .../app/grid/size/max-min-grid-height.spec.js | 4 +- .../app/grid/sort/combinations-sort.spec.js | 4 +- .../demos/pc/app/grid/webdoc/grid-empty.js | 2 +- .../sites/demos/pc/app/icon/iconGroups.js | 1 + .../src/grid/plugins/exportExcel.ts | 3 +- packages/renderless/src/grid/utils/column.ts | 187 +- packages/renderless/src/grid/utils/common.ts | 121 +- packages/renderless/src/grid/utils/dom.ts | 198 +- packages/theme-saas/src/grid/body.less | 10 + packages/theme-saas/src/grid/header.less | 53 - packages/theme-saas/src/grid/table.less | 147 +- .../src/svgs/delegated-processing.svg | 4 + .../theme-saas/src/svgs/pushpin-solid.svg | 18 +- packages/theme-saas/src/svgs/pushpin.svg | 21 +- packages/theme/src/grid/body.less | 10 + packages/theme/src/grid/header.less | 2 - packages/theme/src/grid/table.less | 107 +- .../theme/src/svgs/delegated-processing.svg | 4 + packages/vue-icon-saas/index.ts | 4 + packages/vue-icon/index.ts | 4 + .../src/delegated-processing/index.ts | 15 + packages/vue/src/grid-toolbar/src/index.ts | 10 +- packages/vue/src/grid/index.ts | 2 +- .../vue/src/grid/src/adapter/src/renderer.ts | 24 +- packages/vue/src/grid/src/body/src/body.tsx | 1608 +++++++++-------- packages/vue/src/grid/src/body/src/usePool.ts | 108 ++ packages/vue/src/grid/src/cell/src/cell.ts | 6 +- .../vue/src/grid/src/checkbox/src/methods.ts | 14 +- .../src/grid/src/column-anchor/src/methods.ts | 6 +- .../vue/src/grid/src/column/src/column.ts | 3 +- packages/vue/src/grid/src/composable/index.ts | 5 + .../src/grid/src/composable/useCellEvent.ts | 365 ++++ .../src/grid/src/composable/useCellSpan.ts | 245 +++ .../src/grid/src/composable/useCellStatus.ts | 50 + .../vue/src/grid/src/composable/useData.ts | 126 ++ .../src/grid/src/composable/useDrag/index.ts | 120 +- .../vue/src/grid/src/composable/useHeader.ts | 116 ++ .../src/grid/src/composable/useRowGroup.ts | 105 +- packages/vue/src/grid/src/config.ts | 12 + .../vue/src/grid/src/dragger/src/methods.ts | 2 +- packages/vue/src/grid/src/edit/src/methods.ts | 17 +- .../src/grid/src/fetch-data/src/methods.ts | 4 +- packages/vue/src/grid/src/footer/index.ts | 32 - .../vue/src/grid/src/footer/src/footer.ts | 359 ---- packages/vue/src/grid/src/grid/grid.ts | 117 +- packages/vue/src/grid/src/header/index.ts | 32 - .../vue/src/grid/src/header/src/header.ts | 579 ------ .../vue/src/grid/src/keyboard/src/methods.ts | 61 +- packages/vue/src/grid/src/menu/src/methods.ts | 46 +- .../vue/src/grid/src/mobile-first/index.vue | 3 +- .../vue/src/grid/src/pager/src/methods.ts | 22 +- .../vue/src/grid/src/resize/src/methods.ts | 20 +- .../vue/src/grid/src/table/src/methods.ts | 1119 ++++++------ .../vue/src/grid/src/table/src/strategy.ts | 400 +--- packages/vue/src/grid/src/table/src/table.ts | 1073 +++++------ .../grid/src/table/src/utils/autoCellWidth.ts | 212 ++- .../src/table/src/utils/computeScrollLoad.ts | 4 +- .../src/table/src/utils/handleFixedColumn.ts | 27 - .../grid/src/table/src/utils/updateStyle.ts | 265 +-- .../vue/src/grid/src/toolbar/src/methods.ts | 2 +- packages/vue/src/grid/src/tools/index.ts | 7 +- .../vue/src/grid/src/tooltip/src/methods.ts | 10 +- packages/vue/src/grid/src/tree/src/methods.ts | 35 +- 77 files changed, 3923 insertions(+), 4415 deletions(-) create mode 100644 packages/theme-saas/src/svgs/delegated-processing.svg create mode 100644 packages/theme/src/svgs/delegated-processing.svg create mode 100644 packages/vue-icon/src/delegated-processing/index.ts create mode 100644 packages/vue/src/grid/src/body/src/usePool.ts create mode 100644 packages/vue/src/grid/src/composable/useCellEvent.ts create mode 100644 packages/vue/src/grid/src/composable/useCellSpan.ts create mode 100644 packages/vue/src/grid/src/composable/useCellStatus.ts create mode 100644 packages/vue/src/grid/src/composable/useData.ts create mode 100644 packages/vue/src/grid/src/composable/useHeader.ts delete mode 100644 packages/vue/src/grid/src/footer/index.ts delete mode 100644 packages/vue/src/grid/src/footer/src/footer.ts delete mode 100644 packages/vue/src/grid/src/header/index.ts delete mode 100644 packages/vue/src/grid/src/header/src/header.ts delete mode 100644 packages/vue/src/grid/src/table/src/utils/handleFixedColumn.ts diff --git a/examples/sites/demos/pc/app/grid/custom/column-fixed.spec.js b/examples/sites/demos/pc/app/grid/custom/column-fixed.spec.js index 8b4ad4a35b..4cb592db8d 100644 --- a/examples/sites/demos/pc/app/grid/custom/column-fixed.spec.js +++ b/examples/sites/demos/pc/app/grid/custom/column-fixed.spec.js @@ -3,10 +3,11 @@ import { test, expect } from '@playwright/test' test('列冻结', async ({ page }) => { page.on('pageerror', (exception) => expect(exception).toBeNull()) const custom = page.locator('.tiny-grid-custom') + const demo = page.locator('#custom-column-fixed') await page.goto('grid-custom#custom-column-fixed') await page.locator('.tiny-grid-custom__setting-btn').click() await custom.getByRole('row', { name: '员工数 ' }).getByTitle('未冻结').getByRole('img').click() await custom.getByRole('row', { name: '员工数' }).getByTitle('左冻结').getByRole('img').click() await page.getByRole('button', { name: '确定' }).click() - await expect(page.getByRole('cell', { name: '员工数' })).toHaveCSS('right', '0px') + await expect(demo.locator('.tiny-grid-header__row th').nth(3)).toHaveText(/员工数/) }) diff --git a/examples/sites/demos/pc/app/grid/custom/page-size.spec.js b/examples/sites/demos/pc/app/grid/custom/page-size.spec.js index ec6bf31229..cb76ff4466 100644 --- a/examples/sites/demos/pc/app/grid/custom/page-size.spec.js +++ b/examples/sites/demos/pc/app/grid/custom/page-size.spec.js @@ -9,5 +9,5 @@ test('分页条数', async ({ page }) => { await page.getByText('其他设置', { exact: true }).click() await page.locator('label').filter({ hasText: '5' }).locator('path').nth(1).click() await page.getByRole('button', { name: '确定' }).click() - await expect(demo.locator('.tiny-grid-body__row')).toHaveCount(5) + await expect(demo.locator('.tiny-grid-body__row:visible')).toHaveCount(5) }) diff --git a/examples/sites/demos/pc/app/grid/data-source/request-service.spec.js b/examples/sites/demos/pc/app/grid/data-source/request-service.spec.js index 3b880a4557..a72cb6d9f7 100644 --- a/examples/sites/demos/pc/app/grid/data-source/request-service.spec.js +++ b/examples/sites/demos/pc/app/grid/data-source/request-service.spec.js @@ -6,5 +6,5 @@ test('开启服务请求', async ({ page }) => { await page.getByRole('button', { name: '筛选华南区数据' }).click() // 判断筛选华南区数据成功 - await expect(page.locator('.tiny-grid-body__row')).toHaveCount(3) + await expect(page.locator('.tiny-grid-body__row:visible')).toHaveCount(3) }) diff --git a/examples/sites/demos/pc/app/grid/data-source/static-data.spec.js b/examples/sites/demos/pc/app/grid/data-source/static-data.spec.js index 34c32ef20e..dc14e1ff37 100644 --- a/examples/sites/demos/pc/app/grid/data-source/static-data.spec.js +++ b/examples/sites/demos/pc/app/grid/data-source/static-data.spec.js @@ -14,5 +14,5 @@ test('绑定静态数据', async ({ page }) => { // 改变 data 数据引用地址 await page.getByRole('button', { name: '改变 tableData 引用地址' }).click() - await expect(page.locator('.tiny-grid-body__row')).toHaveCount(2) + await expect(page.locator('.tiny-grid-body__row:visible')).toHaveCount(2) }) diff --git a/examples/sites/demos/pc/app/grid/editor/custom-edit.spec.js b/examples/sites/demos/pc/app/grid/editor/custom-edit.spec.js index 59a7a634aa..c8f8665a72 100644 --- a/examples/sites/demos/pc/app/grid/editor/custom-edit.spec.js +++ b/examples/sites/demos/pc/app/grid/editor/custom-edit.spec.js @@ -3,6 +3,11 @@ import { test, expect } from '@playwright/test' test('多行编辑', async ({ page }) => { page.on('pageerror', (exception) => expect(exception).toBeNull()) await page.goto('grid-editor#editor-custom-edit') - await expect(page.getByRole('cell', { name: 'GFD 科技有限公司' }).getByRole('textbox')).toBeVisible() - await expect(page.getByRole('cell', { name: 'WWWW 科技有限公司' }).getByRole('textbox')).toBeVisible() + const demo = page.locator('#editor-custom-edit') + await expect( + demo.locator('.tiny-grid-body__row').nth(0).locator('td').nth(1).locator('.tiny-input__inner') + ).toBeVisible() + await expect( + demo.locator('.tiny-grid-body__row').nth(1).locator('td').nth(1).locator('.tiny-input__inner') + ).toBeVisible() }) diff --git a/examples/sites/demos/pc/app/grid/empty/empty-data-iscenter.spec.js b/examples/sites/demos/pc/app/grid/empty/empty-data-iscenter.spec.js index 593345220b..7e02c7f5c0 100644 --- a/examples/sites/demos/pc/app/grid/empty/empty-data-iscenter.spec.js +++ b/examples/sites/demos/pc/app/grid/empty/empty-data-iscenter.spec.js @@ -8,5 +8,5 @@ test('固定居中', async ({ page }) => { await expect(page.getByText('暂无数据').first()).toBeVisible() // 判断是否居中 - await expect(page.locator('.empty-center-block')).toHaveCSS('justify-content', 'center') + await expect(page.locator('.tiny-grid__empty-block')).toHaveCSS('justify-content', 'center') }) diff --git a/examples/sites/demos/pc/app/grid/event/toolbar-button-click-event.spec.js b/examples/sites/demos/pc/app/grid/event/toolbar-button-click-event.spec.js index 5515a5188f..e465d89c04 100644 --- a/examples/sites/demos/pc/app/grid/event/toolbar-button-click-event.spec.js +++ b/examples/sites/demos/pc/app/grid/event/toolbar-button-click-event.spec.js @@ -15,5 +15,5 @@ test('工具栏点击事件', async ({ page }) => { await page.getByRole('button', { name: '删除', exact: true }).click() - await expect(page.locator('.tiny-grid-body__row')).toHaveCount(6) + await expect(page.locator('.tiny-grid-body__row:visible')).toHaveCount(6) }) diff --git a/examples/sites/demos/pc/app/grid/expand/set-row-expansion.spec.js b/examples/sites/demos/pc/app/grid/expand/set-row-expansion.spec.js index 12bf540c95..39e91bd091 100644 --- a/examples/sites/demos/pc/app/grid/expand/set-row-expansion.spec.js +++ b/examples/sites/demos/pc/app/grid/expand/set-row-expansion.spec.js @@ -2,6 +2,10 @@ import { test, expect } from '@playwright/test' test('设置指定展开行', async ({ page }) => { page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.setViewportSize({ + width: 1400, + height: 2500 + }) await page.goto('grid-expand#expand-set-row-expansion') await page.getByRole('button', { name: '展开指定行' }).click() await expect(page.getByText('GFD 科技 YX 公司')).toHaveCount(2) diff --git a/examples/sites/demos/pc/app/grid/filter/default-relation.spec.js b/examples/sites/demos/pc/app/grid/filter/default-relation.spec.js index 79bc24af7c..0222300c47 100644 --- a/examples/sites/demos/pc/app/grid/filter/default-relation.spec.js +++ b/examples/sites/demos/pc/app/grid/filter/default-relation.spec.js @@ -7,5 +7,5 @@ test('输入过滤的默认选项', async ({ page }) => { await page.getByRole('spinbutton').click() await page.getByRole('spinbutton').fill('800') await page.getByRole('button', { name: '确定' }).click() - await expect(page.locator('.tiny-grid-body__row')).toHaveCount(2) + await expect(page.locator('.tiny-grid-body__row:visible')).toHaveCount(2) }) diff --git a/examples/sites/demos/pc/app/grid/filter/server-filter.spec.js b/examples/sites/demos/pc/app/grid/filter/server-filter.spec.js index c2614135a3..a742baaad5 100644 --- a/examples/sites/demos/pc/app/grid/filter/server-filter.spec.js +++ b/examples/sites/demos/pc/app/grid/filter/server-filter.spec.js @@ -7,5 +7,5 @@ test('服务端过滤', async ({ page }) => { await page.getByRole('cell', { name: '城市' }).getByRole('img').first().click() await page.locator('li').filter({ hasText: '深圳' }).click() await page.getByRole('button', { name: '确定' }).click() - await expect(page.locator('.tiny-grid-body__row')).toHaveCount(2) + await expect(page.locator('.tiny-grid-body__row:visible')).toHaveCount(2) }) diff --git a/examples/sites/demos/pc/app/grid/footer/footer-summation-empty.spec.js b/examples/sites/demos/pc/app/grid/footer/footer-summation-empty.spec.js index d4d48d8f24..ad3afa0919 100644 --- a/examples/sites/demos/pc/app/grid/footer/footer-summation-empty.spec.js +++ b/examples/sites/demos/pc/app/grid/footer/footer-summation-empty.spec.js @@ -3,7 +3,6 @@ import { test, expect } from '@playwright/test' test('表尾统计(空数据)', async ({ page }) => { page.on('pageerror', (exception) => expect(exception).toBeNull()) await page.goto('grid-footer#footer-footer-summation-empty') - await expect(page.getByRole('row', { name: '平均 0' }).getByRole('cell', { name: '0' })).toBeVisible() await page.getByRole('button', { name: '加载数据' }).click() await expect(page.getByRole('cell', { name: '663' })).toBeVisible() }) diff --git a/examples/sites/demos/pc/app/grid/large-data/full-data-loading.spec.js b/examples/sites/demos/pc/app/grid/large-data/full-data-loading.spec.js index e4297a2db9..5cb20b20a6 100644 --- a/examples/sites/demos/pc/app/grid/large-data/full-data-loading.spec.js +++ b/examples/sites/demos/pc/app/grid/large-data/full-data-loading.spec.js @@ -3,6 +3,10 @@ import { test, expect } from '@playwright/test' test('全量加载', async ({ page }) => { page.on('pageerror', (exception) => expect(exception).toBeNull()) await page.goto('grid-large-data#large-data-full-data-loading') + await page.setViewportSize({ + width: 1400, + height: 2500 + }) await page.locator('.tiny-grid__body').hover() // 先滚动 1000px await page.mouse.wheel(0, 1000) @@ -12,5 +16,5 @@ test('全量加载', async ({ page }) => { // 先滚动 4000px await page.mouse.wheel(0, 5000) await page.waitForTimeout(200) - await expect(page.getByRole('cell', { name: '153' })).toBeVisible() + await expect(page.getByRole('cell', { name: '129' })).toBeVisible() }) diff --git a/examples/sites/demos/pc/app/grid/mouse-keyboard/keyboard-navigation.spec.js b/examples/sites/demos/pc/app/grid/mouse-keyboard/keyboard-navigation.spec.js index 85bfea0d0b..211ba64b85 100644 --- a/examples/sites/demos/pc/app/grid/mouse-keyboard/keyboard-navigation.spec.js +++ b/examples/sites/demos/pc/app/grid/mouse-keyboard/keyboard-navigation.spec.js @@ -4,7 +4,10 @@ test('键盘导航测试', async ({ page }) => { page.on('pageerror', (exception) => expect(exception).toBeNull()) const demo = page.locator('#mouse-keyboard-keyboard-navigation') await page.goto('grid-mouse-keyboard#mouse-keyboard-keyboard-navigation') - + await page.setViewportSize({ + width: 1400, + height: 2500 + }) await page.getByText('GFD 科技 YX 公司').click() await page.waitForTimeout(300) await page.locator('body').press('ArrowDown') diff --git a/examples/sites/demos/pc/app/grid/renderer/inner-renderer.spec.js b/examples/sites/demos/pc/app/grid/renderer/inner-renderer.spec.js index 62660b89a3..26310aba2b 100644 --- a/examples/sites/demos/pc/app/grid/renderer/inner-renderer.spec.js +++ b/examples/sites/demos/pc/app/grid/renderer/inner-renderer.spec.js @@ -3,6 +3,10 @@ import { test, expect } from '@playwright/test' test('内置渲染器', async ({ page }) => { page.on('pageerror', (exception) => expect(exception).toBeNull()) await page.goto('grid-renderer#renderer-inner-renderer') + await page.setViewportSize({ + width: 1400, + height: 2500 + }) const cell = page.getByRole('cell', { name: '90.0%' }).locator('.tiny-grid__rate-chart') await expect(cell).toHaveCSS('background-color', 'rgb(92, 179, 0)') }) diff --git a/examples/sites/demos/pc/app/grid/size/max-min-grid-height.spec.js b/examples/sites/demos/pc/app/grid/size/max-min-grid-height.spec.js index f13f13042c..34ec35b31e 100644 --- a/examples/sites/demos/pc/app/grid/size/max-min-grid-height.spec.js +++ b/examples/sites/demos/pc/app/grid/size/max-min-grid-height.spec.js @@ -3,6 +3,6 @@ import { test, expect } from '@playwright/test' test('设置 maxHeight 最大高度', async ({ page }) => { page.on('pageerror', (exception) => expect(exception).toBeNull()) await page.goto('grid-size#size-max-min-grid-height') - await expect(page.locator('.tiny-grid__body-wrapper').nth(0)).toHaveCSS('max-height', '160px') - await expect(page.locator('.tiny-grid__body-wrapper').nth(1)).toHaveCSS('min-height', '260px') + await expect(page.locator('.tiny-grid__body-wrapper').nth(0)).toHaveCSS('max-height', '200px') + await expect(page.locator('.tiny-grid__body-wrapper').nth(1)).toHaveCSS('min-height', '300px') }) diff --git a/examples/sites/demos/pc/app/grid/sort/combinations-sort.spec.js b/examples/sites/demos/pc/app/grid/sort/combinations-sort.spec.js index fa6f507020..4d59632d02 100644 --- a/examples/sites/demos/pc/app/grid/sort/combinations-sort.spec.js +++ b/examples/sites/demos/pc/app/grid/sort/combinations-sort.spec.js @@ -5,7 +5,7 @@ test('多字段组合排序', async ({ page }) => { await page.goto('grid-sort#sort-combinations-sort') await page.getByRole('cell', { name: '员工数(员工数和名称组合排序)' }).getByRole('img').click() // 员工数第一优先级排序 - await expect(page.locator('.tiny-grid-body__row').first()).toContainText('1300') + await expect(page.locator('.tiny-grid-body__row').first()).toContainText('300') // 公司名称第二优先级排序 - await expect(page.locator('.tiny-grid-body__row').nth(1)).toContainText('YHN 科技 YX 公司') + await expect(page.locator('.tiny-grid-body__row').nth(6)).toContainText('YHN 科技 YX 公司') }) diff --git a/examples/sites/demos/pc/app/grid/webdoc/grid-empty.js b/examples/sites/demos/pc/app/grid/webdoc/grid-empty.js index 112366def5..7407a2bc1e 100644 --- a/examples/sites/demos/pc/app/grid/webdoc/grid-empty.js +++ b/examples/sites/demos/pc/app/grid/webdoc/grid-empty.js @@ -27,7 +27,7 @@ export default { name: { 'zh-CN': '固定居中', 'en-US': 'Fix Center' }, desc: { 'zh-CN': - '

配置 is-center-emptytrue 时,拖动横向滚动条可以保持空数据提示使终相对表格宽度居中显示。

\n', + '

(从3.25.0版本开始默认固定居中)配置 is-center-emptytrue 时,拖动横向滚动条可以保持空数据提示使终相对表格宽度居中显示。

\n', 'en-US': '

When is-center-empty is set to true, drag the horizontal scroll bar to keep the empty data prompt so that the final data is displayed in the center of the table width

\n' }, diff --git a/examples/sites/demos/pc/app/icon/iconGroups.js b/examples/sites/demos/pc/app/icon/iconGroups.js index 604bff844e..8fddd08ca0 100644 --- a/examples/sites/demos/pc/app/icon/iconGroups.js +++ b/examples/sites/demos/pc/app/icon/iconGroups.js @@ -291,6 +291,7 @@ export const iconGroups = { 'IconTabletView', 'IconUnlock', 'IconUser', + 'IconDelegatedProcessing', 'IconVersiontree', 'IconWebPlus', 'IconJs', diff --git a/packages/renderless/src/grid/plugins/exportExcel.ts b/packages/renderless/src/grid/plugins/exportExcel.ts index dfc6afa42f..10a57bd269 100644 --- a/packages/renderless/src/grid/plugins/exportExcel.ts +++ b/packages/renderless/src/grid/plugins/exportExcel.ts @@ -11,8 +11,7 @@ * */ -import { extend } from '@opentiny/utils' -import { browserInfo } from '@opentiny/utils' +import { extend, browserInfo } from '@opentiny/utils' const isIE = browserInfo.name === 'ie' const rgbRegExp = /^rgba?\((\d+),\s(\d+),\s(\d+)([\s\S]*)\)$/ diff --git a/packages/renderless/src/grid/utils/column.ts b/packages/renderless/src/grid/utils/column.ts index 3fd4b447a0..40a657df5c 100644 --- a/packages/renderless/src/grid/utils/column.ts +++ b/packages/renderless/src/grid/utils/column.ts @@ -15,77 +15,126 @@ import { initFilter } from './common' let columnUniqueId = 0 -export const setColumnFormat = (column, props) => (column.format = props.formatConfig) - -function setBasicProperty(column, context) { - column.id = `col_${++columnUniqueId}` - column.type = context.type - column.prop = context.prop - column.rules = context.rules - column.required = context.required - column.property = context.field || context.prop - column.title = context.title - column.label = context.label - column.width = context.width - column.minWidth = context.minWidth - column.resizable = context.resizable - column.fixed = context.fixed - column.align = context.align - column.headerAlign = context.headerAlign - column.footerAlign = context.footerAlign - column.showOverflow = context.showOverflow - column.showHeaderOverflow = context.showHeaderOverflow - column.showTip = context.showTip - column.showHeaderTip = context.showHeaderTip - column.className = context.class || context.className - column.headerClassName = context.headerClassName - column.footerClassName = context.footerClassName - column.indexMethod = context.indexMethod - column.formatText = context.formatText - column.formatValue = context.formatValue - - setColumnFormat(column, context) - - column.sortable = context.sortable - column.sortBy = context.sortBy - column.sortMethod = context.sortMethod - column.remoteSort = context.remoteSort - column.filterMultiple = isBoolean(context.filterMultiple) ? context.filterMultiple : true - column.filterMethod = context.filterMethod - column.filterRender = context.filterRender - column.filter = context.filter && initFilter(context.filter) - column.treeNode = context.treeNode - column.renderer = context.renderer - column.editor = context.editor - column.operationConfig = context.operationConfig - column.equals = context.equals +class FixedDetails { + isLeft: boolean + isLeftLast: boolean + isRight: boolean + isRightFirst: boolean + left: number + right: number + + constructor(fixedType) { + this.isLeft = fixedType === 'left' + this.isLeftLast = false + this.isRight = fixedType === 'right' + this.isRightFirst = false + this.left = 0 + this.right = 0 + } + + getStyle(rightExtra = 0) { + const { isLeft, left, isRight, right } = this + + return { + left: isLeft ? `${left}px` : undefined, + right: isRight ? `${right + rightExtra}px` : undefined + } + } + + getClass() { + const { isLeftLast, isRightFirst } = this + + return { + 'fixed-left-last__column': isLeftLast, + 'fixed-right-first__column': isRightFirst + } + } } -function ColumnConfig(context, { renderHeader, renderCell, renderData } = {}, config = {}) { - // 基本属性 - setBasicProperty(this, context) - // 自定义参数 - this.params = context.params - // 渲染属性 - this.visible = true - this.level = 1 - this.rowSpan = 1 - this.colSpan = 1 - this.order = null - this.renderWidth = 0 // 表格列最终的宽度,会将多种尺寸(number、%、auto)全部转化为固定的px尺寸 - this.renderHeight = 0 - this.resizeWidth = 0 - this.renderLeft = 0 - this.model = {} - this.renderHeader = renderHeader || context.renderHeader - this.renderCell = renderCell || context.renderCell - this.renderData = renderData - this.showIcon = isBoolean(context.showIcon) ? context.showIcon : true - this.loading = false - // 单元格插槽,只对 grid 有效 - this.slots = context.slots - this.own = context - this.asyncPrefix = config.constant.asyncPrefix +class ColumnConfig { + constructor(context, { renderHeader, renderCell, renderData } = {}, config = {}) { + // 基本属性 + this.id = `col_${++columnUniqueId}` + this.type = context.type + this.prop = context.prop + this.rules = context.rules + this.required = context.required + this.property = context.field || context.prop + this.title = context.title + this.label = context.label + this.width = context.width + this.minWidth = context.minWidth + this.resizable = context.resizable + + this._fixed = context.fixed + this._fixedDetails = context.fixed ? new FixedDetails(context.fixed) : undefined + + this.align = context.align + this.headerAlign = context.headerAlign + this.footerAlign = context.footerAlign + this.showOverflow = context.showOverflow + this.showHeaderOverflow = context.showHeaderOverflow + this.showTip = context.showTip + this.showHeaderTip = context.showHeaderTip + this.className = context.class || context.className + this.headerClassName = context.headerClassName + this.footerClassName = context.footerClassName + this.indexMethod = context.indexMethod + this.formatText = context.formatText + this.formatValue = context.formatValue + this.format = context.formatConfig + this.sortable = context.sortable + this.sortBy = context.sortBy + this.sortMethod = context.sortMethod + this.remoteSort = context.remoteSort + this.filterMultiple = isBoolean(context.filterMultiple) ? context.filterMultiple : true + this.filterMethod = context.filterMethod + this.filterRender = context.filterRender + this.filter = context.filter && initFilter(context.filter) + this.treeNode = context.treeNode + this.renderer = context.renderer + this.editor = context.editor + this.operationConfig = context.operationConfig + this.equals = context.equals + + // 自定义参数 + this.params = context.params + + // 渲染属性 + this.visible = true + this.level = 1 + this.rowSpan = 1 + this.colSpan = 1 + this.order = null + this.renderWidth = 0 + this.renderHeight = 0 + this.resizeWidth = 0 + this.renderLeft = 0 + this.model = {} + this.renderHeader = renderHeader || context.renderHeader + this.renderCell = renderCell || context.renderCell + this.renderData = renderData + this.showIcon = isBoolean(context.showIcon) ? context.showIcon : true + this.loading = false + + // 单元格插槽,只对 grid 有效 + this.slots = context.slots + this.own = context + this.asyncPrefix = config.constant.asyncPrefix + } + + set fixed(val) { + this._fixed = val + this._fixedDetails = val ? new FixedDetails(val) : undefined + } + + get fixed() { + return this._fixed + } + + get fixedDetails() { + return this._fixedDetails + } } export const getColumnConfig = (context, options, config) => diff --git a/packages/renderless/src/grid/utils/common.ts b/packages/renderless/src/grid/utils/common.ts index 42c7829d9f..23264b2edb 100644 --- a/packages/renderless/src/grid/utils/common.ts +++ b/packages/renderless/src/grid/utils/common.ts @@ -23,9 +23,8 @@ * */ -import { isNull } from '@opentiny/utils' -import { find } from '@opentiny/utils' -import { get, isFunction, set } from '../static' +import { isNull, find, isFunction } from '@opentiny/utils' +import { get, set } from '../static' export const gridSize = ['medium', 'small', 'mini'] @@ -43,20 +42,58 @@ export const getRowid = ($table, row) => { } // 获取所有的列,排除分组 -export const getColumnList = (columns) => { +export const getColumnList = (columns, options = {}, level = 0) => { const result = [] - columns.forEach((column) => { - if (column.children && column.children.length) { - result.push(...getColumnList(column.children)) - } else { - result.push(column) + columns.forEach((column, index) => { + const hasChildren = column.children?.length + + // 所有层级中,存在固定列配置,就认为存在固定列 + if (!options.hasFixed && column.fixed) { + options.hasFixed = true } + + // 是否存在 type selection 列 + if (!options.isCheckable && column.type === 'selection') { + options.isCheckable = true + } + + // 第一层级存在子级,就认为是多级表头 + if (level === 0 && !options.isGroup && hasChildren) { + options.isGroup = true + } + + options.columnCaches.push({ colid: column.id, column, index }) + + result.push.apply(result, hasChildren ? getColumnList(column.children, options, level + 1) : [column]) }) return result } +export const repairFixed = (root) => { + const subtree = [] + let fixed + + const recursive = (col) => { + subtree.push(col) + + if (!fixed && col.fixed) { + fixed = col.fixed + } + + if (Array.isArray(col.children) && col.children.length > 0) { + col.children.forEach((col) => recursive(col)) + } + } + + recursive(root) + + if (fixed) { + subtree.forEach((c) => (c.fixed = fixed)) + } +} + export const getClass = (property, params) => (property ? (isFunction(property) ? property(params) : property) : '') export const getFilters = (filters) => @@ -69,19 +106,28 @@ export const getFilters = (filters) => })) export const initFilter = (filter) => { - // 改成这种方式可以让用户配置一些筛选的默认行为,如果用户不配置就采用默认的 - return { - condition: { - input: '', - relation: 'equals', - empty: null, - type: null, - value: [] - }, - hasFilter: false, + const { + values, + value: valueKey = 'value', + checked: checkedKey = 'checked', + condition, + enumable, + multi, + inputFilter + } = filter + + const value: any[] = values?.filter?.((i) => i[checkedKey]).map((i) => i[valueKey]) || [] + + const hasChecked = values?.some?.((i) => i[checkedKey]) ?? false + + const filterOptions = { + condition: { input: '', relation: 'equals', empty: null, type: null, value }, + hasFilter: (inputFilter && !!condition?.input) || (enumable && multi && hasChecked) || false, custom: null, - ...filter + showClear: true } + + return { ...filterOptions, ...filter } } export const formatText = (value) => `${isNull(value) ? '' : value}` @@ -182,3 +228,38 @@ export const getListeners = ($attrs, $listeners) => { return listeners } + +/** DFS深度优先遍历树形结构,并生成备份 */ +export function dfsCopy(tree, callback, parent = undefined, isTree = false, childrenKey = 'children') { + let copy + + if (Array.isArray(tree)) { + copy = [] + + tree.forEach((node, index) => { + const copyItem = callback(node, index, parent) + + if (copyItem) { + copy.push(copyItem) + } + + if (isTree) { + const children = node[childrenKey] + + if (children) { + const childrenCopy = dfsCopy(children, callback, node, isTree, childrenKey) + + if (copyItem) { + copyItem[childrenKey] = childrenCopy + } + } + } + }) + } + + return copy +} + +let rowUniqueId = 0 + +export const getRowUniqueId = () => `row_${++rowUniqueId}` diff --git a/packages/renderless/src/grid/utils/dom.ts b/packages/renderless/src/grid/utils/dom.ts index f8cd4fb266..b0a61f8d56 100644 --- a/packages/renderless/src/grid/utils/dom.ts +++ b/packages/renderless/src/grid/utils/dom.ts @@ -36,8 +36,13 @@ export const isPx = (val) => val && /^\d+(px)?$/.test(val) export const isScale = (val) => val && /^\d+%$/.test(val) -export const updateCellTitle = (event) => { - const cellEl = event.currentTarget.querySelector(CELL_CLS) +export const updateCellTitle = (event: Event, td: HTMLElement) => { + const cellEl = td + ? td.querySelector('.tiny-grid-cell-text') || td.querySelector(CELL_CLS) + : (event.currentTarget as HTMLElement)?.querySelector(CELL_CLS) + if (!cellEl) { + return + } const content = cellEl.innerText if (cellEl.getAttribute('title') !== content) { @@ -47,142 +52,89 @@ export const updateCellTitle = (event) => { export const rowToVisible = ($table, row) => { $table.$nextTick(() => { - const tableBodyVnode = $table.$refs.tableBody - - if (tableBodyVnode) { - const gridbodyEl = tableBodyVnode.$el - const trEl = gridbodyEl.querySelector(`[${ATTR_NAME}="${getRowid($table, row)}"]`) - - // 处理虚拟滚动 - if ($table.scrollYLoad) { - // 对应行是否在表格视图外 - const isOutOfBody = () => { - const bodyRect = $table.$el.getBoundingClientRect() - const trRect = trEl.getBoundingClientRect() - return trRect.top + trRect.height / 2 > bodyRect.top + bodyRect.height - } - - if (!trEl || isOutOfBody()) { - gridbodyEl.scrollTop = ($table.afterFullData.indexOf(row) - 1) * $table.scrollYStore.rowHeight - } - } else if (trEl) { - // 非虚拟滚动且有trEl元素 - const bodyHeight = gridbodyEl.clientHeight - const bodySrcollTop = gridbodyEl.scrollTop - const trOffsetTop = trEl.offsetTop + (trEl.offsetParent ? trEl.offsetParent.offsetTop : 0) - const trHeight = trEl.clientHeight - - if (trOffsetTop < bodySrcollTop || trOffsetTop > bodySrcollTop + bodyHeight) { - // 如果跨行滚动 - gridbodyEl.scrollTop = trOffsetTop - } else if (trOffsetTop + trHeight >= bodyHeight + bodySrcollTop) { - gridbodyEl.scrollTop = bodySrcollTop + trHeight - } - } + const { $refs, scrollYLoad, rowHeight, headerHeight, footerHeight, _tileInfo, _graphInfo } = $table + const { tableBody: bodyVm } = $refs + const { $el } = bodyVm + const { map } = _tileInfo + const { graphed } = _graphInfo + const trEl = $el.querySelector(`[${ATTR_NAME}="${getRowid($table, row)}"]`) + const visibleStart = headerHeight + const visibleEnd = $el.clientHeight - footerHeight + const scrollTop = $el.scrollTop + + let position, trHeight + let flag = false + + if (scrollYLoad) { + // 如果是虚拟渲染跨行滚动 + position = headerHeight + rowHeight * graphed.indexOf(map.get(row)) - scrollTop + trHeight = rowHeight + flag = true + } else if (trEl) { + position = trEl.offsetTop - scrollTop + trHeight = trEl.clientHeight + flag = true } - }) -} - -function getFixedLeft($table, from, column, body, offset) { - let scrollLeft = $table.elemStore['main-body-wrapper'].scrollLeft + offset - if (!column.fixed) { - from.fixed === 'left' && (scrollLeft = 0) - from.fixed === 'right' && (scrollLeft = body.scrollWidth) - } + if (flag) { + if (position < visibleStart) { + $el.scrollTop = scrollTop - (visibleStart - position) + return + } - return scrollLeft -} + position += trHeight -// 计算水平滚动位置(考虑存在冻结表的情况) -function computeScrollLeft($table, td) { - const { tableBody } = $table.$refs - const { visibleColumn } = $table - const { scrollLeft: bodyLeft, clientWidth: bodyWidth } = tableBody.$el - // Tiny表格冻结列采用sticky,需遍历计算整体宽度 - let leftWidth = 0 - let rightWidth = 0 - visibleColumn.forEach((column) => { - if (column.fixed === 'left') { - leftWidth += column.renderWidth - } else if (column.fixed === 'right') { - rightWidth += column.renderWidth + if (position > visibleEnd) { + $el.scrollTop = scrollTop + (position - visibleEnd) + } } }) - const tdLeft = td._accumulateRenderWidth || td.offsetLeft + (td.offsetParent ? td.offsetParent.offsetLeft : 0) - const tdWidth = td._renderWidth || td.clientWidth - - let scrollLeft - - // 列元素在主表体可视区左侧(包括被左冻结表部分遮挡的情况) - if (tdLeft < bodyLeft + leftWidth) { - scrollLeft = tdLeft - leftWidth - } else if (tdLeft + tdWidth > bodyLeft + bodyWidth - rightWidth) { - // 列元素在主表体可视区右侧(包括被右冻结表部分遮挡的情况) - scrollLeft = tdLeft + tdWidth - bodyWidth + rightWidth - } else { - // 列元素在主表体可视区内 - scrollLeft = bodyLeft - } - - return scrollLeft } -function setBodyLeft(body, td, $table, column, move) { - const { isLeftArrow, isRightArrow, from } = move || {} - - const bodyScollLeft = computeScrollLeft($table, td) - $table.scrollTo(bodyScollLeft) - $table.lastScrollLeft = bodyScollLeft - if (from) { - const direction = isLeftArrow ? 'left' : isRightArrow ? 'right' : null - const fixedDom = $table.elemStore[`${direction}-body-list`] - const mainBody = $table.elemStore['main-body-wrapper'] - const { left, right } = td.getBoundingClientRect() - let offset = 0 - - if (isLeftArrow && fixedDom) { - const div = fixedDom.querySelector('td.fixed__column') - const division = div ? div.getBoundingClientRect().left : fixedDom.getBoundingClientRect().right - - division > left && (offset = left - division) - } - - if (isRightArrow && fixedDom) { - const div = fixedDom.querySelector('td:not(.fixed__column)') || fixedDom - const division = div.getBoundingClientRect().left - - division < right && (offset = right - division) - } - - mainBody.scrollLeft = getFixedLeft($table, from, column, body, offset) +export const colToVisible = ($table, column) => { + // 固定列始终可见,无需继续处理 + if (column.fixed) { + return } -} -export const colToVisible = ($table, column, move) => { $table.$nextTick(() => { - const gridbodyEl = $table.$refs.tableBody.$el - const tdElem = gridbodyEl.querySelector(`.${column.id}`) + const { $refs, scrollXLoad, visibleColumn, columnStore } = $table + const { tableBody: bodyVm } = $refs + const { $el } = bodyVm + const { leftList, rightList } = columnStore + const tdEl = $el.querySelector(`.${column.id}`) + const visibleStart = leftList.reduce((p, c) => (p += c.renderWidth), 0) + const visibleEnd = $el.clientWidth - rightList.reduce((p, c) => (p += c.renderWidth), 0) + const scrollLeft = $el.scrollLeft + const colWidth = column.renderWidth + + let position + let flag = false + + if (scrollXLoad) { + flag = true + position = -scrollLeft + + for (const col of visibleColumn) { + if (col === column) break + position += col.renderWidth + } + } else if (tdEl) { + flag = true + position = tdEl.offsetLeft - scrollLeft + } - if (tdElem) { - setBodyLeft(gridbodyEl, tdElem, $table, column, move) - } else if ($table.scrollXLoad) { - // 如果是虚拟渲染跨行滚动 - const visibleColumn = $table.visibleColumn - let scrollLeft = 0 + if (flag) { + if (position < visibleStart) { + $el.scrollLeft = scrollLeft - (visibleStart - position) + return + } - for (let index = 0; index < visibleColumn.length; index++) { - if (visibleColumn[index] === column) { - break - } + position += colWidth - scrollLeft += visibleColumn[index].renderWidth + if (position > visibleEnd) { + $el.scrollLeft = scrollLeft + (position - visibleEnd) } - - gridbodyEl.scrollLeft = computeScrollLeft($table, { - _accumulateRenderWidth: scrollLeft, - _renderWidth: column.renderWidth - }) } }) } diff --git a/packages/theme-saas/src/grid/body.less b/packages/theme-saas/src/grid/body.less index b14ed9fdd7..8d0036bbce 100644 --- a/packages/theme-saas/src/grid/body.less +++ b/packages/theme-saas/src/grid/body.less @@ -10,6 +10,16 @@ @apply border-b border-b-color-bg-3; @apply overflow-y-auto; @apply overflow-x-auto; + + &.no-data { + @apply overflow-y-hidden; + @apply flex; + @apply flex-col; + + > .@{grid-prefix-cls}-body__y-space { + @apply hidden; + } + } } .@{grid-prefix-cls}__borders { diff --git a/packages/theme-saas/src/grid/header.less b/packages/theme-saas/src/grid/header.less index 11c4876262..64565c86c0 100644 --- a/packages/theme-saas/src/grid/header.less +++ b/packages/theme-saas/src/grid/header.less @@ -4,8 +4,6 @@ @grid-header-prefix-cls: ~'@{css-prefix}grid-header'; @grid-cell-prefix-cls: ~'@{css-prefix}grid-cell'; @grid-checkbox-prefix-cls: ~'@{css-prefix}grid-checkbox'; -@header-suffix: ~'@{grid-prefix-cls}-cell__header-suffix'; -@cell-tooltip: ~'@{grid-prefix-cls}-cell__tooltip'; .@{grid-prefix-cls}__header-wrapper { @apply bg-color-fill-8; @@ -168,57 +166,6 @@ } } -.@{grid-prefix-cls}__header { - .@{header-suffix} { - @apply relative; - min-height: 16px; - - .suffix-icon-1 { - @apply absolute; - @apply right-3; - } - - .suffix-icon-0 { - @apply absolute; - @apply right-0; - } - } - - .col__ellipsis { - &.is__editable.is__sortable.is__filter { - .@{header-suffix}.@{cell-tooltip} { - @apply pr-7; - } - } - - &.is__editable.is__sortable:not(.is__filter), - &.is__editable.is__filter:not(.is__sortable) { - .@{header-suffix}.@{cell-tooltip} { - @apply ~'pr-3.5'; - } - } - - &:not(.is__sortable):not(.is__filter) { - .@{header-suffix}.@{cell-tooltip} { - @apply pr-2; - } - } - - &.is__sortable.is__filter:not(.is__editable) { - .@{header-suffix}.@{cell-tooltip} { - padding-right: 26px; - } - } - - &.is__sortable:not(.is__filter):not(.is__editable), - &.is__filter:not(.is__sortable):not(.is__editable) { - .@{header-suffix}.@{cell-tooltip} { - @apply pr-3; - } - } - } -} - .@{grid-prefix-cls} { th.col__selection > .@{grid-cell-prefix-cls} { @apply relative; diff --git a/packages/theme-saas/src/grid/table.less b/packages/theme-saas/src/grid/table.less index 17bcedd741..1fa33d9dda 100644 --- a/packages/theme-saas/src/grid/table.less +++ b/packages/theme-saas/src/grid/table.less @@ -7,6 +7,8 @@ @input-prefix-cls: ~'@{css-prefix}input'; @select-prefix-cls: ~'@{css-prefix}select'; @pager-prefix-cls: ~'@{css-prefix}pager'; +@header-suffix: ~'@{grid-prefix-cls}-cell__header-suffix'; +@cell-tooltip: ~'@{grid-prefix-cls}-cell__tooltip'; // table .@{grid-prefix-cls} { @@ -267,23 +269,6 @@ } } - // tiny新增滚动条放置表格内 - tbody tr:last-child { - @apply relative; - - &::after { - @apply content-['']; - @apply absolute; - @apply bottom-0; - @apply left-0; - @apply right-0; - @apply bottom-0; - @apply h-px; - @apply bg-color-bg-1; - @apply ~'z-[3]'; - } - } - &&__border, &&__border-saas { // 启用 border 只有表头生效,默认不建议启用 border 属性,另外如果内嵌列,则表头会自动启用 border 属性 @@ -385,7 +370,7 @@ } &&__group-saas { - .@{grid-prefix-cls}__header { + .@{grid-prefix-cls}__body thead { @apply relative; &::before { @@ -431,7 +416,7 @@ } &&__border-vertical { - .@{grid-prefix-cls}__body { + .@{grid-prefix-cls}__body tbody { @apply relative; &::before { @@ -687,8 +672,7 @@ & &-body__x-space { @apply w-full; - @apply h-px; - @apply -mb-px; + @apply h-0; } & &-body__y-space { @@ -878,37 +862,16 @@ } & &__empty-block { - @apply hidden; - @apply opacity-0; @apply h-full; @apply ~"min-h-[theme('spacing.16')]"; @apply py-16 px-0; - @apply justify-center; - @apply items-center; - @apply text-center; - - &.is__visible { - @apply flex; - @apply flex-col; - @apply opacity-100; - &.is__center { - @apply opacity-0; - } - } - } - - .empty-center-block { - @apply ~'z-[1]'; @apply flex; @apply flex-col; + @apply flex-auto; + @apply items-center; @apply justify-center; - @apply text-center; - @apply absolute; - @apply w-full; - - .@{grid-prefix-cls}__empty-text { - @apply w-full; - } + @apply sticky; + @apply left-0; } & &__empty-img { @@ -921,8 +884,8 @@ & &__empty-text { @apply block; - @apply mt-2; - @apply ~'w-1/2'; + @apply w-full; + @apply text-center; } & &-body__column { @@ -1029,14 +992,15 @@ & &__body-wrapper { &.body__wrapper.is__scrollload { @apply overflow-y-hidden; - @apply static; } } & .is__scrollload &-body__y-space { @apply absolute; @apply right-0; + @apply bottom-0; @apply w-3; @apply overflow-y-scroll; + @apply z-20; .@{grid-prefix-cls}-body__y-scrollbar { @apply w-px; @@ -1392,4 +1356,89 @@ } } } + + .@{grid-prefix-cls}__body { + .tiny-grid-header__column { + @apply sticky; + /* --tiny-color-fill-8 真实对应 rgba(31, 85, 181, .05) */ + background-color: var(--tiny-color-fill-8-solid, #f4f6fb); + } + + .tiny-grid-header__column:last-child { + contain: layout; + } + + .tiny-grid-header__column .tiny-grid-thead-partition, + .tiny-grid-header__column .tiny-grid-resizable { + transform: translateX(calc(50% - 1px)); + } + + .tiny-grid-custom-footer { + @apply w-full; + @apply sticky; + @apply bottom-0; + } + + .tiny-grid-footer__column { + @apply sticky; + @apply bg-color-bg-1; + } + + .@{header-suffix} { + @apply relative; + min-height: 16px; + + .suffix-icon-1 { + @apply absolute; + @apply right-3; + } + + .suffix-icon-0 { + @apply absolute; + @apply right-0; + } + } + + .col__ellipsis { + &.is__editable.is__sortable.is__filter { + .@{header-suffix}.@{cell-tooltip} { + @apply pr-7; + } + } + + &.is__editable.is__sortable:not(.is__filter), + &.is__editable.is__filter:not(.is__sortable) { + .@{header-suffix}.@{cell-tooltip} { + @apply ~'pr-3.5'; + } + } + + &:not(.is__sortable):not(.is__filter) { + .@{header-suffix}.@{cell-tooltip} { + @apply pr-2; + } + } + + &.is__sortable.is__filter:not(.is__editable) { + .@{header-suffix}.@{cell-tooltip} { + padding-right: 26px; + } + } + + &.is__sortable:not(.is__filter):not(.is__editable), + &.is__filter:not(.is__sortable):not(.is__editable) { + .@{header-suffix}.@{cell-tooltip} { + @apply pr-3; + } + } + } + } + + .sticky-wrapper { + @apply sticky; + @apply top-0; + @apply left-0; + @apply overflow-hidden; + @apply h-full; + } } diff --git a/packages/theme-saas/src/svgs/delegated-processing.svg b/packages/theme-saas/src/svgs/delegated-processing.svg new file mode 100644 index 0000000000..5cb83e86d5 --- /dev/null +++ b/packages/theme-saas/src/svgs/delegated-processing.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/theme-saas/src/svgs/pushpin-solid.svg b/packages/theme-saas/src/svgs/pushpin-solid.svg index 83d23971b0..480712ad86 100644 --- a/packages/theme-saas/src/svgs/pushpin-solid.svg +++ b/packages/theme-saas/src/svgs/pushpin-solid.svg @@ -1,10 +1,14 @@ - + - Created with Pixso. + Created with Pixso. - - + + + + + + + + + \ No newline at end of file diff --git a/packages/theme-saas/src/svgs/pushpin.svg b/packages/theme-saas/src/svgs/pushpin.svg index 64e38c2eba..1652550f0d 100644 --- a/packages/theme-saas/src/svgs/pushpin.svg +++ b/packages/theme-saas/src/svgs/pushpin.svg @@ -1,7 +1,14 @@ - - - - - \ No newline at end of file + + + Created with Pixso. + + + + + + + + + + + \ No newline at end of file diff --git a/packages/theme/src/grid/body.less b/packages/theme/src/grid/body.less index 895293937f..ecf2cc17d1 100644 --- a/packages/theme/src/grid/body.less +++ b/packages/theme/src/grid/body.less @@ -21,6 +21,16 @@ .@{grid-prefix-cls}__fixed-right-body-wrapper { overflow-y: auto; overflow-x: auto; + + &.no-data { + overflow-y: hidden; + display: flex; + flex-direction: column; + + > .@{grid-prefix-cls}-body__y-space { + display: none; + } + } } // 鼠标配置项开启后,选中单元格的边框样式(position:absolute) diff --git a/packages/theme/src/grid/header.less b/packages/theme/src/grid/header.less index f28c187ba3..0c97568908 100644 --- a/packages/theme/src/grid/header.less +++ b/packages/theme/src/grid/header.less @@ -16,8 +16,6 @@ @grid-header-prefix-cls: ~'@{css-prefix}grid-header'; @grid-cell-prefix-cls: ~'@{css-prefix}grid-cell'; @grid-checkbox-prefix-cls: ~'@{css-prefix}grid-checkbox'; -@header-suffix: ~'@{grid-prefix-cls}-cell__header-suffix'; -@cell-tooltip: ~'@{grid-prefix-cls}-cell__tooltip'; .@{grid-prefix-cls}__header-wrapper { background-color: var(--tv-Grid-header-bg-color); diff --git a/packages/theme/src/grid/table.less b/packages/theme/src/grid/table.less index 64f55a6c43..2ee12af0a3 100644 --- a/packages/theme/src/grid/table.less +++ b/packages/theme/src/grid/table.less @@ -19,6 +19,7 @@ @input-prefix-cls: ~'@{css-prefix}input'; @select-prefix-cls: ~'@{css-prefix}select'; @pager-prefix-cls: ~'@{css-prefix}pager'; +@header-suffix: ~'@{grid-prefix-cls}-cell__header-suffix'; // table .@{grid-prefix-cls} { @@ -312,11 +313,8 @@ .@{grid-prefix-cls}-header__column, .@{grid-prefix-cls}-body__column, .@{grid-prefix-cls}-footer__column { - background-image: linear-gradient( - -90deg, - var(--tv-Grid-border-color-divider), - var(--tv-Grid-border-color-divider) - ), + background-image: + linear-gradient(-90deg, var(--tv-Grid-border-color-divider), var(--tv-Grid-border-color-divider)), linear-gradient(-180deg, var(--tv-Grid-border-color-divider), var(--tv-Grid-border-color-divider)); background-repeat: no-repeat; background-size: @@ -340,7 +338,7 @@ top: 0; width: 0; height: 100%; - z-index: 1; + z-index: 10; } &:before { @@ -604,8 +602,7 @@ & &-body__x-space { width: 100%; - height: 1px; - margin-bottom: -1px; + height: 0; } & &-body__y-space { @@ -722,38 +719,16 @@ // 暂无数据 & &__empty-block { - display: none; - opacity: 0; height: 100%; min-height: 60px; padding: 60px 0; - justify-content: center; - align-items: center; - text-align: center; - - &.is__visible { - display: flex; - flex-flow: column wrap; - opacity: 1; - &.is__center { - opacity: 0; - } - } - } - - .empty-center-block { - z-index: 1; display: flex; - flex-direction: column; + align-items: center; justify-content: center; - text-align: center; - position: absolute; - width: 100%; - height: calc(100% - 60px); - - .@{grid-prefix-cls}__empty-text { - width: 100%; - } + position: sticky; + left: 0; + flex: auto; + flex-direction: column; } // 表格无数据背景图 @@ -766,7 +741,8 @@ & &__empty-text { display: block; margin-top: 8px; - width: 50%; + text-align: center; + width: 100%; } // 校验不通过 @@ -879,15 +855,16 @@ & &__body-wrapper { &.body__wrapper.is__scrollload { overflow-y: hidden; - position: static; } } & .is__scrollload &-body__y-space { position: absolute; right: 0; + bottom: 0; width: 12px; overflow-y: scroll; + z-index: 20; .@{grid-prefix-cls}-body__y-scrollbar { width: 1px; @@ -1197,7 +1174,8 @@ transform: translateY(-50%); width: 8px; height: 14px; - background-image: linear-gradient( + background-image: + linear-gradient( 180deg, var(--row-drop-handle-bgcolor) 0 2px, transparent 2px 6px, @@ -1227,6 +1205,59 @@ } } } + + .@{grid-prefix-cls}__body { + .tiny-grid-header__column { + background-color: var(--tv-Grid-header-bg-color); + position: sticky; + } + + .tiny-grid-header__column:last-child { + contain: layout; + } + + .tiny-grid-header__column .tiny-grid-thead-partition, + .tiny-grid-header__column .tiny-grid-resizable { + transform: translateX(calc(50% - 1px)); + } + + .tiny-grid-custom-footer { + width: 100%; + position: sticky; + bottom: 0; + } + + .tiny-grid-footer__column { + position: sticky; + background-color: var(--tv-Grid-bg-color); + } + + .@{grid-prefix-cls}-cell-text { + font-weight: var(--tv-Grid-header-font-weight); + } + .@{header-suffix} { + position: relative; + min-height: 16px; + + .suffix-icon-1 { + position: absolute; + right: 12px; + } + + .suffix-icon-0 { + position: absolute; + right: 0; + } + } + } + + .sticky-wrapper { + position: sticky; + top: 0; + left: 0; + overflow: hidden; + height: 100%; + } } // 表格全屏样式 diff --git a/packages/theme/src/svgs/delegated-processing.svg b/packages/theme/src/svgs/delegated-processing.svg new file mode 100644 index 0000000000..5cb83e86d5 --- /dev/null +++ b/packages/theme/src/svgs/delegated-processing.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/vue-icon-saas/index.ts b/packages/vue-icon-saas/index.ts index dc7b2cca99..f0677fd0ec 100644 --- a/packages/vue-icon-saas/index.ts +++ b/packages/vue-icon-saas/index.ts @@ -90,6 +90,7 @@ import IconDeltaLeftO from './src/delta-left-o' import IconDeltaLeft from './src/delta-left' import IconDeltaRightO from './src/delta-right-o' import IconDeltaRight from './src/delta-right' +import IconDelegatedProcessing from './src/delegated-processing' import IconDeltaUpO from './src/delta-up-o' import IconDeltaUp from './src/delta-up' import IconDerive from './src/derive' @@ -857,6 +858,8 @@ export { IconDeltaRightO as iconDeltaRightO, IconDeltaRight, IconDeltaRight as iconDeltaRight, + IconDelegatedProcessing, + IconDelegatedProcessing as iconDelegatedProcessing, IconDeltaUpO, IconDeltaUpO as iconDeltaUpO, IconDeltaUp, @@ -1740,6 +1743,7 @@ export default { IconDeltaLeft, IconDeltaRightO, IconDeltaRight, + IconDelegatedProcessing, IconDeltaUpO, IconDeltaUp, IconDerive, diff --git a/packages/vue-icon/index.ts b/packages/vue-icon/index.ts index dc7b2cca99..f0677fd0ec 100644 --- a/packages/vue-icon/index.ts +++ b/packages/vue-icon/index.ts @@ -90,6 +90,7 @@ import IconDeltaLeftO from './src/delta-left-o' import IconDeltaLeft from './src/delta-left' import IconDeltaRightO from './src/delta-right-o' import IconDeltaRight from './src/delta-right' +import IconDelegatedProcessing from './src/delegated-processing' import IconDeltaUpO from './src/delta-up-o' import IconDeltaUp from './src/delta-up' import IconDerive from './src/derive' @@ -857,6 +858,8 @@ export { IconDeltaRightO as iconDeltaRightO, IconDeltaRight, IconDeltaRight as iconDeltaRight, + IconDelegatedProcessing, + IconDelegatedProcessing as iconDelegatedProcessing, IconDeltaUpO, IconDeltaUpO as iconDeltaUpO, IconDeltaUp, @@ -1740,6 +1743,7 @@ export default { IconDeltaLeft, IconDeltaRightO, IconDeltaRight, + IconDelegatedProcessing, IconDeltaUpO, IconDeltaUp, IconDerive, diff --git a/packages/vue-icon/src/delegated-processing/index.ts b/packages/vue-icon/src/delegated-processing/index.ts new file mode 100644 index 0000000000..a1143fd542 --- /dev/null +++ b/packages/vue-icon/src/delegated-processing/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { svg } from '@opentiny/vue-common' +import DelegatedProcessing from '@opentiny/vue-theme/svgs/delegated-processing.svg' + +export default () => svg({ name: 'IconDelegatedProcessing', component: DelegatedProcessing })() diff --git a/packages/vue/src/grid-toolbar/src/index.ts b/packages/vue/src/grid-toolbar/src/index.ts index 3e43b1b197..96fb3e4987 100644 --- a/packages/vue/src/grid-toolbar/src/index.ts +++ b/packages/vue/src/grid-toolbar/src/index.ts @@ -555,7 +555,7 @@ export default defineComponent({ if (comp) { const colWidth = this.loadColWidth() - comp.reloadCustoms(customs, sort, colWidth).then((fullColumn) => { + comp.reloadCustoms(customs, sort, colWidth)?.then((fullColumn) => { this.tableFullColumn = fullColumn }) } @@ -670,13 +670,7 @@ export default defineComponent({ if (this.$grid) { if (columns && columns.length) { const colWidth = this.loadColWidth() - this.$grid.reloadCustoms(columns, sort, colWidth).then(() => { - // 处理表格数据,否则列排序不生效 - this.$grid.handleTableData(true).then(() => { - // 重新计算内部元素的位置 - this.$grid.recalculate() - }) - }) + this.$grid.reloadCustoms(columns, sort, colWidth) } if (isNumber(pageSize) && this.$grid.pagerConfig && this.$grid.pagerConfig.pageSize !== pageSize) { diff --git a/packages/vue/src/grid/index.ts b/packages/vue/src/grid/index.ts index d0799e53cc..3084806403 100644 --- a/packages/vue/src/grid/index.ts +++ b/packages/vue/src/grid/index.ts @@ -86,7 +86,7 @@ const getWrapFunc = (name) => function (...args) { const tinyTable = this.$refs.tinyTable if (tinyTable) { - return this.$refs.tinyTable[name].apply(tinyTable, args) + return tinyTable[name]?.apply(tinyTable, args) } } diff --git a/packages/vue/src/grid/src/adapter/src/renderer.ts b/packages/vue/src/grid/src/adapter/src/renderer.ts index 776a7ea3bc..9d82d35881 100644 --- a/packages/vue/src/grid/src/adapter/src/renderer.ts +++ b/packages/vue/src/grid/src/adapter/src/renderer.ts @@ -22,8 +22,8 @@ * SOFTWARE. * */ -import { set, assign, objectMap, get, each, isObject, isFunction } from '@opentiny/vue-renderless/grid/static/' -import { getCellValue, setCellValue } from '@opentiny/vue-renderless/grid/utils' +import { assign, objectMap, get, each, isObject, isFunction } from '@opentiny/vue-renderless/grid/static/' +import { getCellValue, getRowid, setCellValue } from '@opentiny/vue-renderless/grid/utils' import { hooks } from '@opentiny/vue-common' function getAttrs({ name, attrs }, params) { @@ -68,15 +68,18 @@ function getEvents(renderOpts, params, context) { [type](event) { let cellValue = native ? event.target.value : event - if (!renderOpts.isValidAlways && isSyncCell(renderOpts, params, context)) { - setCellValue(row, column, cellValue) - } else { - native || set(row, column.property, cellValue) + if (!isSyncCell(renderOpts, params, context)) { model.update = true model.value = cellValue - $table.updateStatus(params, cellValue, renderOpts) } + setCellValue(row, column, cellValue) + + Promise.resolve().then(() => { + $table.updateStatus(params, cellValue, renderOpts) + }) + + // 对原生组件调用input和change回调 if (native) { input && input.apply(null, [params].concat.apply(params, arguments)) change && change.apply(null, [params].concat.apply(params, arguments)) @@ -195,7 +198,10 @@ function defaultFilterMethod({ option, row, column }) { } function renderSelectEdit(h, renderOpts, params, context) { + const { column, $table, row } = params + const editorKey = `editor-${getRowid($table, row)}-${column.id}` let props = { + ref: editorKey, class: 'tiny-grid-default-select', on: getEvents(renderOpts, params, context) } @@ -221,8 +227,12 @@ function defaultEditRender(h, renderOpts, params, context) { let editorModel = component.model || {} let modelProps = typeof component === 'string' ? 'value' : editorModel.prop || 'modelValue' + const editorKey = `editor-${getRowid($table, row)}-${column.id}` + + // 获取行的唯一标识作为key const key = row[$table.rowId] let options = { + ref: editorKey, key, class: isTag ? `tiny-grid-default-${component}` : '', attrs: { diff --git a/packages/vue/src/grid/src/body/src/body.tsx b/packages/vue/src/grid/src/body/src/body.tsx index 98bf54c949..8eff55dc31 100644 --- a/packages/vue/src/grid/src/body/src/body.tsx +++ b/packages/vue/src/grid/src/body/src/body.tsx @@ -1,4 +1,3 @@ -/* eslint-disable unused-imports/no-unused-vars */ /** * MIT License * @@ -24,494 +23,515 @@ * */ -import { isFunction, find } from '@opentiny/vue-renderless/grid/static/' -import { isNull } from '@opentiny/utils' +import { isFunction, find, isBoolean } from '@opentiny/vue-renderless/grid/static/' +import { removeClass, addClass, isObject, throttle } from '@opentiny/utils' import { - updateCellTitle, emitEvent, getClass, getFuncText, getRowid, - formatText + formatText, + getOffsetPos } from '@opentiny/vue-renderless/grid/utils' import { getCellLabel } from '../../tools' import GlobalConfig from '../../config' -import { iconChevronRight, iconChevronDown, iconGridNoData } from '@opentiny/vue-icon' +import { iconGridNoData, iconChevronRight, iconChevronDown } from '@opentiny/vue-icon' import { h, hooks, $prefix, defineComponent } from '@opentiny/vue-common' -import { getTreeChildrenKey, getTreeShowKey, handleRowGroupFold, isVirtualRow } from '../../table/src/strategy' -import { generateFixedClassName } from '../../table/src/utils/handleFixedColumn' +import { handleRowGroupFold, isVirtualRow, getFixedStyle, getFixedClass } from '../../table/src/strategy' +import { usePool } from './usePool' +import { getConfigOverflow, useCellEvent, useCellSpan, useHeader } from '../../composable' + +const ChevronRight = iconChevronRight() +const ChevronDown = iconChevronDown() const GridNoData = iconGridNoData() -// 滚动、拖动过程中不需要触发鼠标移入移出事件 -const isOperateMouse = ($table) => - $table._isResize || ($table.lastScrollTime && Date.now() < $table.lastScrollTime + $table.optimizeOpts.delayHover) - -let renderRowFlag = false - -// 解决静态扫描驼峰变量问题 -const classMap = { - colEdit: 'col__edit', - colIndex: 'col__index', - colRadio: 'col__radio', - colSelection: 'col__selection', - colEllipsis: 'col__ellipsis', - editVisible: 'edit__visible', - fixedColumn: 'fixed__column', - colDirty: 'col__dirty', - colActived: 'col__actived', - rowNew: 'row__new', - rowSelected: 'row__selected', - rowRadio: 'row__radio', - rowActived: 'row__actived', - isScrollload: 'is__scrollload' -} -const renderBorder = (h, type) => { - let vnTop = h('span', { - class: 'tiny-grid-border-top', - ref: `${type}Top` - }) - let vnRight = h('span', { - class: 'tiny-grid-border-right', - ref: `${type}Right` - }) - let vnBottom = h('span', { - class: 'tiny-grid-border-bottom', - ref: `${type}Bottom` - }) - let vnLeft = h('span', { - class: 'tiny-grid-border-left', - ref: `${type}Left` - }) +let renderRowFlag = true - return h( - 'div', - { - class: `tiny-grid-${type}ed-borders`, - ref: `${type}Borders` - }, - [vnTop, vnRight, vnBottom, vnLeft] +const renderBorder = (type) => { + return ( +
+ + + + +
) } -function buildColumnProps(args) { - const { attrs, cellAlign, cellClassName, className, column, columnActived, columnIndex, columnKey, editor } = args - const { fixedHiddenColumn, hasEllipsis, isDirty, params, tdOns, validError, validated, columnStore } = args - - const { leftList, rightList } = columnStore - - return { - class: [ - 'tiny-grid-body__column', - column.id, - { - [`col__${cellAlign}`]: cellAlign, - [classMap.colEdit]: editor, - [classMap.colIndex]: column.type === 'index', - [classMap.colRadio]: column.type === 'radio', - [classMap.colSelection]: column.type === 'selection', - [classMap.colEllipsis]: hasEllipsis, - [classMap.editVisible]: editor && editor.type === 'visible', - [classMap.fixedColumn]: fixedHiddenColumn, - [classMap.colDirty]: isDirty, - [classMap.colActived]: columnActived, - 'col__valid-error': validError && validated, - 'col__valid-success': columnActived ? !validError && !validated : isDirty && !validated, - 'col__treenode': column.treeNode, - 'fixed-left-last__column': column.fixed === 'left' && leftList[leftList.length - 1] === column, - 'fixed-right-first__column': column.fixed === 'right' && rightList[0] === column - }, - getClass(className, params), - getClass(cellClassName, params) - ], - style: fixedHiddenColumn - ? { - left: `${column.style?.left}px`, - right: `${column.style?.right}px` - } - : null, - key: columnKey ? column.id : columnIndex, - attrs, - on: tdOns - } -} +/** 渲染列 */ +function renderColumn({ $columnIndex, $table, _vm, column, id, row, rowid, seq, used }) { + const { align: allAlign, cellClassName, dropConfig = {}, editConfig, editRules, editStore } = $table + const { height, rowId, scrollXLoad, scrollYLoad, validOpts, validStore, validatedMap } = $table + const { normalRows } = _vm + const { attrs = { rowspan: 1, colspan: 1, visible: true }, params = { $table, row, column } } = + normalRows[rowid]?.[column.id] || {} + const { isMessageDefault, isMessageInline } = validOpts + const { actived } = editStore + const validated = validatedMap[column.id + '-' + row[rowId]] + const validError = validStore.row === row && validStore.column === column + const hasDefaultTip = editRules && (isMessageDefault ? height || !_vm.isNoData : isMessageInline) + const { align, className, editor } = column -function buildColumnChildren(args) { - let { h, hasDefaultTip, params, row, validError, column, $table } = args - let { showEllipsis, showTip, showTitle, showTooltip, validStore } = args - const dropConfig = args.dropConfig || {} - const { validOpts } = $table - let cellNode: any[] = [] - let validNode: any = null - if (hasDefaultTip) { - validNode = [null] - if (validError) { - validNode = h( - 'div', - { - class: 'tiny-grid-cell__valid', - style: validStore.rule && validStore.rule.width ? { width: `${validStore.rule.width}px` } : null - }, - [ - validOpts?.icon ? h(validOpts.icon, { class: 'tiny-grid-cell__valid-icon' }) : null, - h('span', { class: 'tiny-grid-cell__valid-msg', attrs: { title: validStore.content } }, validStore.content) - ] - ) - } - } - cellNode = [ - dropConfig.rowHandle === 'index' && column.type === 'index' ? h('div', { class: 'row__drop-handle' }) : null, - h( - 'div', - { - class: [ - 'tiny-grid-cell', - { - 'tiny-grid-cell__title': showTitle, - 'tiny-grid-cell__tooltip': showTooltip || showTip, - 'tiny-grid-cell__ellipsis': showEllipsis - } - ], - attrs: { title: showTitle ? getCellLabel(row, column, params) : null } - }, - // 调用column组件的renderCell渲染单元格内部的内容 - // 如果不是表格形态,就只保留表格结构(到tiny-grid-cell),不渲染具体的内容 - $table.isShapeTable ? column.renderCell(h, params) : null - ), - validNode - ] - return cellNode -} + let cellAlign = align || allAlign -function modifyCellAlign({ cellAlign, column }) { - if (~['radio', 'selection', 'index'].indexOf(column.type)) { + // 索引列、选择列如果不配置对齐方式则默认为居中对齐 + if (['radio', 'selection', 'index'].includes(column.type)) { cellAlign = cellAlign || 'center' } - return cellAlign -} + let { cellTip, cellOverflowTitle, cellOverflowTooltip, cellOverflowEllipsis, cellOverflowHint } = getConfigOverflow( + column, + $table + ) -function modifyShowEllipsis({ hasEllipsis, scrollXLoad, scrollYLoad, showEllipsis }) { - if ((scrollXLoad || scrollYLoad) && !hasEllipsis) { - showEllipsis = true + // 滚动的渲染不支持动态行高 + if ((scrollXLoad || scrollYLoad) && !cellOverflowHint) { + cellOverflowEllipsis = true } - return showEllipsis -} - -function addListenerMouseenter({ $table, evntParams, showTip, showTitle, showTooltip, tableListeners, tdOns }) { - if (showTip || showTitle || showTooltip || tableListeners['cell-mouseenter']) { - tdOns.mouseenter = (event) => { - if (isOperateMouse($table)) { - return - } - - evntParams.cell = event.currentTarget - - if (showTitle) { - updateCellTitle(event) - } else if (showTip || showTooltip) { - // 如果配置了显示 tooltip - $table.triggerTooltipEvent(event, evntParams) - } + const columnActived = + editConfig && editor && actived.row === row && (actived.column === column || editConfig.mode === 'row') - emitEvent($table, 'cell-mouseenter', [evntParams, event]) - } - } + // 如果显示状态 + const isDirty = $table.getCellStatus(row, column).isDirty + + params.$columnIndex = $columnIndex + + return ( + 1 ? attrs.rowspan : undefined} + colspan={attrs.colspan > 1 ? attrs.colspan : undefined} + class={[ + 'tiny-grid-body__column', + column.id, + { + [`col__${cellAlign}`]: cellAlign, + 'col__edit': editor, + 'col__index': column.type === 'index', + 'col__radio': column.type === 'radio', + 'col__selection': column.type === 'selection', + 'col__ellipsis': cellOverflowHint, + 'edit__visible': editor && editor.type === 'visible', + 'col__dirty': isDirty, + 'col__actived': columnActived, + 'col__valid-error': validError && validated, + 'col__valid-success': columnActived ? !validError && !validated : isDirty && !validated, + 'col__treenode': column.treeNode + }, + getClass(className, params), + getClass(cellClassName, params), + getFixedClass(column, $table), + attrs._stickyClass || '' + ]}> + {[ + // 行拖拽手柄 + dropConfig.rowHandle === 'index' && column.type === 'index' ?
: null, + // 单元格主内容 + // 如果不是表格形态,就只保留表格结构(到tiny-grid-cell),不渲染具体的内容 +
+ {$table.isShapeTable ? column.renderCell(h, params) : null} +
, + // 行内校验 + hasDefaultTip && validError ? ( +
+ + {validStore.content} + +
+ ) : null + ]} + + ) } -function addListenerMouseleave({ $table, evntParams, showTip, showTooltip, tableListeners, tdOns }) { - if (showTip || showTooltip || tableListeners['cell-mouseleave']) { - tdOns.mouseleave = (event) => { - if (isOperateMouse($table)) { - return - } - - if (showTip || showTooltip) { - $table.clostTooltip() - } +function renderHeaderRows(_vm: any): any { + const { $parent: $table, headerTable } = _vm + const { headerCellClassName, headerRowClassName, headerSuffixIconAbsolute } = $table + const { align: allAlign, border, headerAlign: allHeaderAlign, resizable } = $table + const { editConfig, operationColumnResizable, mouseConfig = {}, dropConfig = {} } = $table + return headerTable.map((cols, $rowIndex) => { + return ( + + {cols.map(({ id, column, colspan, rowspan, height, top }, $columnIndex) => { + const isColGroup = column.children?.length + const { headerAlign, align, headerClassName } = column + const { headerTip, headerOverflowTitle, headerOverflowTooltip, headerOverflowEllipsis, headerOverflowHint } = + getConfigOverflow(column, $table) + const columnIndex = $table.getColumnIndex(column) + const params = { $table, $rowIndex, column, columnIndex, $columnIndex, isHidden: false } + const isColResize = ['index', 'radio', 'selection'].includes(column.type) ? operationColumnResizable : true + + let headAlign = headerAlign || align || allHeaderAlign || allAlign + + if (['radio', 'selection', 'index'].includes(column.type)) { + headAlign = headAlign || 'center' + } - evntParams.cell = event.currentTarget + return ( + 1 ? colspan : undefined} + rowspan={rowspan > 1 ? rowspan : undefined} + style={[ + getFixedStyle(column, $table), + { height: rowspan > 1 ? `${height}px` : undefined, top: `${top}px`, zIndex: column.fixed ? 20 : 10 } + ]} + class={[ + 'tiny-grid-header__column', + column.id, + { + [`col__${headAlign}`]: headAlign, + 'col__fixed': column.fixed, + 'col__index': column.type === 'index', + 'col__radio': column.type === 'radio', + 'col__selection': column.type === 'selection', + 'col__group': isColGroup, + 'col__ellipsis': headerOverflowHint, + 'is__sortable': !['index', 'radio', 'selection'].includes(column.type) && column.sortable, + 'is__editable': column.editor, + 'is__filter': isObject(column.filter), + 'filter__active': column.filter && column.filter.hasFilter, + 'is__multilevel': rowspan > 1 + }, + getClass(headerClassName, params), + getClass(headerCellClassName, params), + getFixedClass(column, $table) + ]}> + {[ + !isColGroup && + !(isBoolean(column.resizable) ? column.resizable : resizable) && + column.type !== 'index' ? ( +
+ ) : null, + // 如果不是表格形态,就只保留表格结构(到tiny-grid-cell),不渲染具体的内容 +
+ {$table.isShapeTable ? column.renderHeader(h, params) : null} +
, + mouseConfig.checked && dropConfig.column && !column.type && !column.fixed ? ( +
event.stopPropagation()} /> + ) : null, + !isColGroup && isColResize && (isBoolean(column.resizable) ? column.resizable : resizable) ? ( +
_vm.resizeMousedown(event, params)} + /> + ) : null + ]} + + ) + })} + + ) + }) +} - emitEvent($table, 'cell-mouseleave', [evntParams, event]) - } - } +function renderFooterRows(_vm: any): any { + const { $parent: $table, footerData, columnPool, rowHeight, footerRows } = _vm + const { align: allAlign, footerAlign: allFooterAlign, footerCellClassName, footerRowClassName } = $table + const footerDataLength = footerData.length + + return footerData.map((list, $rowIndex) => { + const trBottom = (footerDataLength - 1 - $rowIndex) * rowHeight + + return ( + + {columnPool.map(({ id, item: column, used }, $columnIndex) => { + const { footerAlign, align, footerClassName } = column + const ftAlign = footerAlign || align || allFooterAlign || allAlign + const { cellOverflowHint } = getConfigOverflow(column, $table) + const { attrs = { rowspan: 1, colspan: 1, visible: true }, params = { $table, column } } = + footerRows[$rowIndex]?.[column.id] || {} + const rowspan = attrs?.rowspan || 1 + const tdBottom = trBottom - (rowspan > 1 ? (rowspan - 1) * rowHeight : 0) + + params.$columnIndex = $columnIndex + + return ( + 1 ? attrs.rowspan : undefined} + colspan={attrs.colspan > 1 ? attrs.colspan : undefined} + style={[ + getFixedStyle(column, $table), + attrs._stickyStyle, + { + bottom: `${tdBottom}px`, + zIndex: column.fixed ? 20 : 10, + display: used && attrs.visible ? undefined : 'none' + } + ]} + class={[ + 'tiny-grid-footer__column', + column.id, + { + [`col__${ftAlign}`]: ftAlign, + 'col__ellipsis': cellOverflowHint, + 'filter__active': column.filter && column.filter.hasFilter + }, + getClass(footerClassName, params), + getClass(footerCellClassName, params), + getFixedClass(column, $table), + attrs._stickyClass || '' + ]}> +
+ {$table.isShapeTable ? formatText(list[$columnIndex]) : null} +
+ + ) + })} + + ) + }) } -function addListenerMousedown({ $table, evntParams, mouseConfig, tdOns }) { - if (mouseConfig.checked || mouseConfig.selected) { - tdOns.mousedown = (event) => { - evntParams.cell = event.currentTarget - $table.triggerCellMousedownEvent(event, evntParams) +function renderRows(_vm) { + const { $parent: $table, tableColumn, rowPool } = _vm + const { afterFullData, editConfig, editStore, expandConfig = {}, expandeds, hasVirtualRow } = $table + const { rowClassName, rowGroup, scrollYLoad, scrollYStore, selection, treeConfig, treeOrdered } = $table + const expandMethod = expandConfig.activeMethod + const startIndex = scrollYStore.startIndex + const isOrdered = treeConfig ? !!treeOrdered : false + const { hideMethod } = treeConfig || {} + const { actived } = editStore + const rows = [] + const seqCount = { value: 0 } + const $seq = '' + + rowPool.forEach(({ id, item: { payload: row, level: rowLevel }, used }, $rowIndex) => { + const rowActived = editConfig && actived.row === row + const virtualRow = isVirtualRow(row) + const isSkipRowRender = (hideMethod && hideMethod(row, rowLevel)) || virtualRow + const rowid = getRowid($table, row) + const rowIndex = $table.getRowIndex(row) + + if (!isSkipRowRender) { + seqCount.value = seqCount.value + 1 } - } -} -function addListenerClick(args) { - let { $table, column, editConfig, editor, evntParams, expandConfig, highlightCurrentRow } = args - let { mouseConfig, radioConfig, selectConfig, tableListeners, tdOns, treeConfig } = args - let satisfy = (equal, trigger) => trigger === 'row' || (equal(column) && trigger === 'cell') + let seq = isOrdered ? seqCount.value : $rowIndex + 1 - if ( - highlightCurrentRow || - tableListeners['cell-click'] || - mouseConfig.checked || - (editor && editConfig) || - satisfy(() => true, expandConfig.trigger) || - satisfy(({ type }) => type === 'radio', radioConfig.trigger) || - satisfy(({ type }) => type === 'selection', selectConfig.trigger) || - satisfy(({ treeNode }) => treeNode, treeConfig.trigger) - ) { - tdOns.click = (event) => { - evntParams.cell = event.currentTarget - $table.triggerCellClickEvent(event, evntParams) + if (scrollYLoad) { + seq += startIndex } - } -} -function getRowSpanMethod(rowSpan) { - return ({ row, $rowIndex, column, data }) => { - let fields = [] - - if (rowSpan) { - rowSpan.forEach((item) => { - column.visible && fields.push(item.field) - }) + // 分组表场景正常数据行的序号由在afterFullData中的位置提供 + if (hasVirtualRow && !virtualRow) { + seq = afterFullData.indexOf(row) + 1 } - let cellVal = row[column.property] + renderRowGroupData({ $table, _vm, id, row, rowGroup, rowid, rows, used, virtualRow }) - if (cellVal && ~fields.indexOf(column.property)) { - let prevSiblingRow = data[$rowIndex - 1] - let nextSiblingRow = data[$rowIndex + 1] + let args = { $rowIndex, $seq, $table, _vm, editStore, id, isSkipRowRender, row, rowActived, rowClassName } - if (prevSiblingRow && prevSiblingRow[column.property] === cellVal) { - return { rowspan: 0, colspan: 0 } - } else { - let rowspanCount = 1 + Object.assign(args, { rowIndex, rowLevel, rowid, rows, selection, seq, treeConfig, used }) - while (nextSiblingRow && nextSiblingRow[column.property] === cellVal) { - nextSiblingRow = data[++rowspanCount + $rowIndex] - } + renderRow(args) - if (rowspanCount > 1) { - return { rowspan: rowspanCount, colspan: 1 } - } - } - } - } -} + renderRowAfter({ $table, _vm, id, row, rowIndex, rows, used }) -function addListenerDblclick({ $table, evntParams, tableListeners, tdOns, triggerDblclick }) { - if (triggerDblclick || tableListeners['cell-dblclick']) { - tdOns.dblclick = (event) => { - evntParams.cell = event.currentTarget - $table.triggerCellDBLClickEvent(event, evntParams) - } - } + args = { $table, expandMethod, expandeds, id, row, rowIndex, rowLevel, rows, seq, tableColumn, treeConfig, used } + + // 如果行被展开了 + renderRowExpanded(args) + }) + renderRowFlag = !renderRowFlag + return rows } -function doSpan({ attrs, params, rowSpan, spanMethod }) { - const rowSpanMethod = getRowSpanMethod(rowSpan) +function renderRowExpanded(args) { + const { $table, expandMethod, expandeds, id, row, rowIndex } = args + const { rowLevel, rows, seq, tableColumn, treeConfig, used } = args - if (spanMethod || rowSpan) { - let { rowspan = 1, colspan = 1 } = (spanMethod ? spanMethod(params) : rowSpanMethod(params)) || {} + if ( + expandeds.length && + expandeds.includes(row) && + (typeof expandMethod === 'function' ? expandMethod(row, rowLevel) : true) + ) { + const column = find(tableColumn, (column) => column.type === 'expand') + const columnIndex = $table.getColumnIndex(column) + let cellStyle - if (!rowspan || !colspan) { - return false + if (treeConfig) { + cellStyle = { paddingLeft: `${rowLevel * (treeConfig.indent || 16) + 30}px` } } - attrs.rowspan = rowspan - attrs.colspan = colspan - } - - return true -} - -function isCellDirty({ $table, column, editConfig, isDirty, row }) { - const { showStatus = false, relationFields = true } = editConfig || {} - // 关联字段配置为true,或者配置包含当前字段时,支持脏数据检查 - const canChange = - relationFields === true || (Array.isArray(relationFields) && relationFields.includes(column.property)) + if (column) { + const options = { $table, seq, row, rowIndex, column, columnIndex, level: rowLevel } - if (editConfig && showStatus && column.property && (column.editor || (relationFields && canChange))) { - isDirty = $table.hasRowChange(row, column.property) + rows.push( + + +
+ {column.renderData(h, options)} +
+ + + ) + } } - - return isDirty } -const setColumnEvents = (args1) => { - let { $columnIndex, $rowIndex, $table, column, columnIndex } = args1 - let { row, rowIndex, rowLevel, seq } = args1 - let { editConfig, expandConfig = {} } = $table - let { radioConfig = {}, showOverflow: allColumnOverflow } = $table - let { highlightCurrentRow, mouseConfig = {} } = $table - let { scrollXLoad, scrollYLoad, selectConfig = {} } = $table - let { tableListeners, treeConfig = {} } = $table - let tdOns = {} - let fixedHiddenColumn = column.fixed - let { editor, showOverflow, showTip } = column - let cellOverflow = isNull(showOverflow) ? allColumnOverflow : showOverflow - let showTitle = cellOverflow === 'title' - let showTooltip = cellOverflow === true || cellOverflow === 'tooltip' - let showEllipsis = cellOverflow === 'ellipsis' - let hasEllipsis = showTitle || showTooltip || showEllipsis - let triggerDblclick = editor && editConfig && editConfig.trigger === 'dblclick' - - let commonParams = { $columnIndex, $rowIndex, $table, column, columnIndex } - Object.assign(commonParams, { isHidden: fixedHiddenColumn, level: rowLevel, row, rowIndex, seq }) - - let evntParams = { showTip, ...commonParams } - // 滚动的渲染不支持动态行高 - showEllipsis = modifyShowEllipsis({ hasEllipsis, scrollXLoad, scrollYLoad, showEllipsis }) - // 单元格hover 进入事件 - addListenerMouseenter({ $table, evntParams, showTip, showTitle, showTooltip, tableListeners, tdOns }) - // 单元格hover 退出事件 - addListenerMouseleave({ $table, evntParams, showTip, showTooltip, tableListeners, tdOns }) - // 按下事件处理 - addListenerMousedown({ $table, evntParams, mouseConfig, tdOns }) - - let args = { $table, column, editConfig, editor, evntParams, expandConfig, highlightCurrentRow } - Object.assign(args, { mouseConfig, radioConfig, selectConfig, tableListeners, tdOns, treeConfig }) - // 点击事件处理 - addListenerClick(args) - // 双击事件处理 - addListenerDblclick({ $table, evntParams, tableListeners, tdOns, triggerDblclick }) - - return { - commonParams, - args, - cellOverflow, - showTitle, - showTooltip, - showEllipsis, - hasEllipsis, - tdOns, - fixedHiddenColumn - } +function renderRowAfter({ $table, _vm, row, rowIndex, rows, id, used }) { + typeof $table.renderRowAfter === 'function' && + $table.renderRowAfter.call($table, { rows, row, data: _vm.tableData, rowIndex, renderColumn, id, used }, h) } -// 渲染列 -function renderColumn(args1) { - let { $seq, $table, column, columnIndex } = args1 - let { h, row } = args1 - let { align: allAlign, cellClassName, columnKey, editConfig } = $table - let { editRules, editStore, rowId, rowSpan, height } = $table - let { tableData, validOpts, validStore, validatedMap, spanMethod, columnStore, dropConfig = {} } = $table - let { isDirty, attrs = { 'data-colid': column.id } } = {} - let { isMessageDefault, isMessageInline } = validOpts - let { actived } = editStore - let validated = validatedMap[`${column.id}-${row[rowId]}`] - let validError = validStore.row === row && validStore.column === column - let hasDefaultTip = editRules && (isMessageDefault ? height || tableData.length > 1 : isMessageInline) - let { align, editor, showTip } = column - const className = column.own.className - let cellAlign = align || allAlign - let columnActived = - editConfig && editor && actived.row === row && (actived.column === column || editConfig.mode === 'row') - - let { - commonParams, - args, - showTitle, - showTooltip, - showEllipsis, - tdOns = {}, - hasEllipsis, - fixedHiddenColumn - } = setColumnEvents(args1) - let params = { $seq, data: tableData, ...commonParams } - // 索引列、选择列如果不配置对齐方式则默认为居中对齐 - cellAlign = modifyCellAlign({ cellAlign, column }) +function renderRow(args) { + const { $rowIndex, $seq, $table, _vm, editStore, id, isSkipRowRender, row, rowActived, rowClassName } = args + const { rowIndex, rowLevel, rowid, rows, selection, seq, treeConfig, used } = args - // 合并行或列 - if (!doSpan({ attrs, params, rowSpan, spanMethod })) { + if (isSkipRowRender) { return } - // 编辑后的显示状态(是否该单元格数据被更改)此处如果是树表大数据虚拟滚动+表格编辑器,会造成卡顿,这里需要递归树表数据 - isDirty = isCellDirty({ $table, column, editConfig, isDirty, row }) - args = { - attrs, - cellAlign, - cellClassName, - className, - column, - columnActived, - columnIndex, - columnKey, - editor, - columnStore - } - Object.assign(args, { fixedHiddenColumn, hasEllipsis, isDirty, params, tdOns, validError, validated }) - // 组装渲染单元格td所需要的props属性 - const colProps = buildColumnProps(args) - args = { column, h, hasDefaultTip, params, row, $table } - Object.assign(args, { showEllipsis, showTip, showTitle, showTooltip, validError, validStore, dropConfig }) + let key = id + if (row._isDraging) { + // 防止数据多次刷新导致key回归rowid + _vm.$nextTick(() => { + delete row._isDraging + }) + if (renderRowFlag) { + key = `drag_${key}` + } + } - // 渲染td单元格中的div元素(自定义渲染和编辑器) - const colChildren = buildColumnChildren(args) + const { columnPool } = _vm - return h('td', colProps, colChildren) + rows.push( + + {columnPool.map(({ id, item: column, used }, $columnIndex) => + renderColumn({ $columnIndex, $table, _vm, column, id, row, rowid, seq, used }) + )} + + ) } -function renderRowGroupTds(args) { - const { $table, closeable, currentIcon, render, renderGroupCell } = args - const { row, tableColumn, tds, title } = args +function renderRowGroupTds({ $table, closeable, render, renderGroupCell, row, tds, title, _vm }) { const targetColumn = $table._rowGroupTargetColumn const value = row.value || '' + const { columnPool } = _vm - for (let index in tableColumn) { - if (Object.prototype.hasOwnProperty.call(tableColumn, index)) { - const column = tableColumn[index] - const columnIndex = $table.getColumnIndex(column) - const header = title || formatText(getFuncText(column.title), 1) || value - const params = { value, header, children: row.children, expand: !row.fold, row, column, columnIndex } - - // 不渲染colspan小于等于0的列 - if (column._rowGroupColspan <= 0) { - continue - } - if (column === targetColumn) { - let groupTitleVNode - - if (render) { - groupTitleVNode = render(h, params) - } else { - groupTitleVNode = [ - {header}, - `:${value}`, - {row.children.length} - ] - } - tds.push( - -
{[closeable ? currentIcon : null].concat(groupTitleVNode)}
- - ) - } else { - tds.push( - -
{renderGroupCell ? renderGroupCell(h, params) : null}
- - ) - } - } + for (let { id, item: column, used } of columnPool) { + const columnIndex = $table.getColumnIndex(column) + const header = title || formatText(getFuncText(column.title), 1) || value + const params = { value, header, children: row.children, expand: !row.fold, row, column, columnIndex } + const isTarget = column === targetColumn + + tds.push( + 0 ? undefined : 'none' }]} + class={[ + 'tiny-grid-body__column', + isTarget ? 'td-group' : 'td-placeholder', + column.id, + getFixedClass(column, $table), + column._stickyClass || '' + ]} + colspan={column._rowGroupColspan} + data-colid={column.id}> +
+ {isTarget + ? [ + closeable ? ( + row.fold ? ( + + ) : ( + + ) + ) : null + ].concat( + render + ? render(h, params) + : [ + {header}, + ':' + value, + {row.children.length} + ] + ) + : renderGroupCell + ? renderGroupCell(h, params) + : null} +
+ + ) } } -function renderRowGroupData({ $table, virtualRow, row, rowGroup, rowid, rows, tableColumn }) { - if (!virtualRow) { - return - } +function renderRowGroupData({ $table, _vm, id, row, rowGroup, rowid, rows, used, virtualRow }) { + if (!virtualRow) return const { title, closeable = true, render, renderGroupCell, className } = rowGroup - const { tds = [], ChevronRight = iconChevronRight(), ChevronDown = iconChevronDown() } = {} - const currentIcon = row.fold ? : - const args = { $table, closeable, currentIcon, render, renderGroupCell } - Object.assign(args, { row, tableColumn, tds, title }) - // 将分组行的td添加到tds数组中 - renderRowGroupTds(args) + const tds = [] + + renderRowGroupTds({ $table, closeable, render, renderGroupCell, row, tds, title, _vm }) const onClick = (event) => { handleRowGroupFold(row, $table) @@ -523,7 +543,9 @@ function renderRowGroupData({ $table, virtualRow, row, rowGroup, rowid, rows, ta rows.push( (row.hover = false)} onMouseover={() => (row.hover = true)} @@ -533,449 +555,439 @@ function renderRowGroupData({ $table, virtualRow, row, rowGroup, rowid, rows, ta ) } -function renderRow(args) { - let { $rowIndex, $seq, $table, _vm, editStore } = args - let { h, row, rowActived } = args - let { rowClassName, rowIndex, rowKey, rowLevel, rowid, rows } = args - let { seq, trOn, isNotRenderRow } = args - const { selection, tableColumn, treeConfig, selectRow } = $table +function renderTable({ $table, _vm }) { + const { tableLayout, scrollXLoad, scrollYLoad, bodyTableWidth, isColumnWidthAssigned } = $table + const { columnPool, isNoData } = _vm - if (isNotRenderRow) { + if (!isColumnWidthAssigned) { return } - let key = rowid - if (row._isDraging) { - // 防止数据多次刷新导致key回归rowid - _vm.$nextTick(() => { - delete row._isDraging - }) - if (renderRowFlag) { - key = `${rowid}${rowKey}` - } - } - - rows.push( - h( - 'tr', - { - class: [ - 'tiny-grid-body__row', - { - [`row__level-${rowLevel}`]: treeConfig, - [classMap.rowNew]: editStore.insertList.includes(row), - [classMap.rowSelected]: selection.includes(row), - [classMap.rowRadio]: selectRow === row, - [classMap.rowActived]: rowActived - }, - rowClassName - ? isFunction(rowClassName) - ? rowClassName({ $table, $seq, seq, rowLevel, row, rowIndex, $rowIndex }) - : rowClassName - : '' - ], - attrs: { - 'data-rowid': rowid - }, - key, - on: trOn - }, - tableColumn.map((column, $columnIndex) => { - let columnIndex = $table.getColumnIndex(column) - let args1 = { $columnIndex, $rowIndex, $seq, $table, _vm, column, columnIndex } - - Object.assign(args1, { h, row, rowIndex, rowLevel, seq }) + const tableVnode = ( + + {[ + // 列分组(用于指定列宽) + + {columnPool.map(({ id, item: column, used }) => { + return ( + + ) + })} + , + // 表头 + $table.showHeader ? {renderHeaderRows(_vm)} : null, + // 表体内容 + {renderRows(_vm)}, + // 表尾 + $table.showFooter && !isNoData && typeof $table.renderFooter !== 'function' ? ( + {renderFooterRows(_vm)} + ) : null + ]} +
+ ) - return renderColumn(args1) - }) - ) + return scrollXLoad || scrollYLoad ? ( +
+ {tableVnode} +
+ ) : ( + tableVnode ) } -function renderRowAfter({ $table, h, row, rowIndex, rows, tableData }) { - typeof $table.renderRowAfter === 'function' && - $table.renderRowAfter({ rows, row, data: tableData, rowIndex, renderColumn }, h) -} +const calcScrollLeft = ($table, wrapperScrollLeft) => { + const { visibleColumn, tableColumn } = $table + let start, end, total, offset, column -function renderRowExpanded(args) { - const { $table, expandMethod, expandeds, h, row, rowIndex } = args - const { rowLevel, rowid, rows, seq, tableColumn, trOn, treeConfig } = args + start = end = total = offset = 0 - if ( - expandeds.length && - expandeds.includes(row) && - (typeof expandMethod === 'function' ? expandMethod(row, rowLevel) : true) - ) { - const column = find(tableColumn, (column) => column.type === 'expand') - const columnIndex = $table.getColumnIndex(column) - let cellStyle + for (const col of visibleColumn) { + start = end + end = start + col.renderWidth - if (treeConfig) { - cellStyle = { paddingLeft: `${rowLevel * (treeConfig.indent || 16) + 30}px` } + if (wrapperScrollLeft >= start && wrapperScrollLeft < end) { + offset = wrapperScrollLeft - total + column = col + break } - if (column) { - const renderData = { $table, seq, row, rowIndex, column, columnIndex, level: rowLevel } - rows.push( - h( - 'tr', - { - class: 'tiny-grid-body__expanded-row', - key: `expand_${rowid}`, - on: trOn - }, - [ - h( - 'td', - { - class: 'tiny-grid-body__expanded-column', - attrs: { colspan: tableColumn.length } - }, - [ - h( - 'div', - { - class: 'tiny-grid-body__expanded-cell', - style: cellStyle - }, - [column.renderData(h, renderData)] - ) - ] - ) - ] - ) - ) - } + total += col.renderWidth } -} - -function renderRowTree(args, renderRows) { - let { $seq, $table, _vm, h, row, rowLevel } = args - let { rows, seq, seqCount, tableColumn, treeConfig, treeExpandeds } = args - let { scrollYLoad } = $table - // 如果没有树表配置或者树表展开行数为零,则直接跳过 - if (!treeConfig || !treeExpandeds.length) { - return - } + total = 0 - let childrenKey = getTreeChildrenKey({ scrollYLoad, treeConfig }) - let rowChildren = row[childrenKey] - - // 若果当前行不是展开行或者子节点个数为零,则跳过 - if (!rowChildren || !rowChildren.length || !~treeExpandeds.indexOf(row)) { - return - } + for (const col of tableColumn) { + if (col === column) { + total += offset + break + } - const args1 = { - h, - _vm, - $table, - // $seq 树表特有序号:1 --> 1.1 - $seq: $seq ? `${$seq}.${seq}` : `${seq}`, - rowLevel: rowLevel + 1, - tableData: rowChildren, - tableColumn, - seqCount + total += col.renderWidth } - rows.push(...renderRows(args1)) + return total } -function renderRows({ h, _vm, $table, $seq, rowLevel, tableData, tableColumn, seqCount }) { - let { rowKey, rowClassName, treeConfig, treeExpandeds } = $table - let { groupData, scrollYLoad, scrollYStore, editConfig, editStore, expandConfig = {} } = $table - let { expandeds, selection, rowGroup, hasVirtualRow, afterFullData, treeOrdered } = $table - let rows = [] - let expandMethod = expandConfig.activeMethod - let startIndex = scrollYStore.startIndex - // 子级索引是否按数字递增显示:true(子级索引按数字递增显示,父级1,子级2);false(子级索引在父级索引基础上增加,父级1,子级1.1) - let isOrdered = treeConfig ? Boolean(treeOrdered) : false - seqCount = seqCount || { value: 0 } - let treeShowKey = getTreeShowKey({ scrollYLoad, treeConfig }) - let { hideMethod } = treeConfig || {} - - // 循环表格数据,生成表格主体内容VNode,此处也是性能优化的整改点 - tableData.forEach((row, $rowIndex) => { - let trOn = {} - let rowIndex = $rowIndex - let { actived } = editStore - let rowActived = editConfig && actived.row === row - let virtualRow = isVirtualRow(row) - const isNotRenderRow = (treeShowKey && !row[treeShowKey]) || (hideMethod && hideMethod(row, rowLevel)) || virtualRow - - // 树表虚拟滚动,如果当前行被剪切不需要渲染,则无需自增序号 - if (!isNotRenderRow) { - seqCount.value = seqCount.value + 1 - } +const calcScrollTop = ($table, wrapperScrollTop) => { + const { _graphInfo, tableData, headerHeight, rowHeight } = $table + const graphed = _graphInfo.graphed + const rows = graphed.map((node) => node.payload) + let start, end, total, offset, row - let seq = isOrdered ? seqCount.value : rowIndex + 1 - if (scrollYLoad) { - seq += startIndex - } - // 分组表场景正常数据行的序号由在afterFullData中的位置提供 - if (hasVirtualRow && !virtualRow) { - seq = afterFullData.indexOf(row) + 1 - } - // 确保任何情况下 rowIndex 都精准指向真实 data 索引 - rowIndex = $table.getRowIndex(row) + start = end = total = offset = 0 - let rowid = getRowid($table, row) + if (wrapperScrollTop < headerHeight) { + total = wrapperScrollTop + } else { + start = end = total = offset = headerHeight - // 如果有表格分组信息,则执行分组逻辑 - renderRowGroupData({ $table, virtualRow, row, rowGroup, rowid, rows, tableColumn }) - let args = { $rowIndex, $seq, $table, _vm, editStore, h, row, rowActived } - Object.assign(args, { rowClassName, rowIndex, rowKey, rowLevel, rowid, rows, selection, seq }) + for (const r of rows) { + start = end + end = start + rowHeight - Object.assign(args, { tableColumn, trOn, treeConfig, isNotRenderRow }) + if (wrapperScrollTop >= start && wrapperScrollTop < end) { + offset = wrapperScrollTop - total + row = r + break + } - // 输出表格行列的vnode节点列表 - renderRow(args) + total += rowHeight + } - // 允许用户自定义表格行渲染后的逻辑 - renderRowAfter({ $table, h, row, rowIndex, rows, tableData }) - args = { $table, expandMethod, expandeds, h, row, rowIndex, rowLevel } - Object.assign(args, { rowid, rows, seq, tableColumn, trOn, treeConfig }) + total = headerHeight - // 如果行被展开了,这里渲染展开行的vnode节点 - renderRowExpanded(args) - args = { $seq, $table, _vm, h, row, rowLevel, rows } - Object.assign(args, { seq, seqCount, tableColumn, treeConfig, treeExpandeds }) + for (const r of tableData) { + if (hooks.toRaw(r) === hooks.toRaw(row)) { + total += offset + break + } - // 如果是树形表格,则会递归渲染已展开行的子节点 - renderRowTree(args, renderRows) - }) - renderRowFlag = !renderRowFlag + total += rowHeight + } + } - return rows + return total } +export default defineComponent({ + name: `${$prefix}GridBody`, + props: { + collectColumn: Array, + tableColumn: Array, + tableNode: Array, + tableData: Array, + footerData: Array + }, + setup(props, { slots }) { + const vm = hooks.getCurrentInstance()?.proxy + const $table = vm?.$parent + const rowHeight = hooks.computed(() => $table.rowHeight) + const headerRowHeight = hooks.computed(() => $table.headerRowHeight) + const { headerTable } = useHeader(props, vm, headerRowHeight) + const { columnPool, rowPool, isNoData } = usePool(props) + const wrapperScrollLeft = hooks.ref(0) + const wrapperScrollTop = hooks.ref(0) + const stickyWrapper = hooks.ref() + const table = hooks.ref() + const body = hooks.ref() + const customFooter = hooks.ref() + const colgroup = hooks.ref() + const thead = hooks.ref() + const tbody = hooks.ref() + const ySpace = hooks.ref() + + hooks.watch(wrapperScrollLeft, (wrapperScrollLeft) => { + const el = stickyWrapper.value + if (!el) return + hooks.nextTick(() => (el.scrollLeft = calcScrollLeft($table, wrapperScrollLeft))) + }) -function renderDefEmpty(h) { - return [ - h(GridNoData, { - class: 'tiny-grid__empty-img' - }), - h( - 'span', - { - class: 'tiny-grid__empty-text' - }, - GlobalConfig.i18n('ui.grid.emptyText') - ) - ] -} + hooks.watch(wrapperScrollTop, (wrapperScrollTop) => { + const el = stickyWrapper.value + if (!el) return + hooks.nextTick(() => (el.scrollTop = calcScrollTop($table, wrapperScrollTop))) + }) -const syncHeaderAndFooterScroll = ({ bodyElem, footerElem, headerElem, isX }) => { - const scrollLeft = bodyElem.scrollLeft - if (isX && headerElem) { - headerElem.scrollLeft = scrollLeft - } - if (isX && footerElem) { - footerElem.scrollLeft = scrollLeft - } -} + useCellEvent({ table, $table }) + const { normalRows, footerRows } = useCellSpan(vm, props) -function doScrollLoad({ $table, _vm, bodyElem, event, headerElem, isX, isY, scrollLeft, scrollXLoad, scrollYLoad }) { - let isScrollX = scrollXLoad && isX + hooks.watch([body, table, thead, tbody, ySpace], () => { + const { elemStore } = $table - // 如果是水平虚拟滚动,并且正在进行水平滚动,就触发水平虚滚事件 - if (isScrollX) { - // 处理x轴方法虚拟滚动加载数据逻辑 - $table.triggerScrollXEvent(event) - } + elemStore['main-body-wrapper'] = body.value + elemStore['main-body-table'] = table.value + elemStore['main-body-headerList'] = thead.value + elemStore['main-body-list'] = tbody.value + elemStore['main-body-ySpace'] = ySpace.value + }) - // 同上,并且主表头存在时,修复极端场景(拖动滚动条到最右侧)表头表体水平滚动位置不同步问题 - if (isScrollX && headerElem && scrollLeft + bodyElem.clientWidth >= bodyElem.scrollWidth) { - // 修复拖动滚动条时可能存在不同步问题 - _vm.$nextTick(() => { - if (bodyElem.scrollLeft !== headerElem.scrollLeft) { - headerElem.scrollLeft = bodyElem.scrollLeft + const bodyClientWidth = hooks.ref(0) + + const resizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + const target = entry.target as HTMLElement + + switch (target) { + case body.value: + bodyClientWidth.value = target.clientWidth + break + case customFooter.value: + $table.footerHeight = target.offsetHeight + } } }) - } - // 如果是垂直虚拟滚动,并且正在进行垂直滚动,就触发垂直虚滚事件 - if (scrollYLoad && isY) { - // 处理y轴方法虚拟滚动加载数据逻辑 - $table.triggerScrollYEvent(event) - } -} + hooks.watch(body, (body) => body && resizeObserver.observe(body)) -function renderEmptyBlock({ $slots, $table, _vm, isCenterCls, renderEmpty, tableData }) { - return h( - 'div', - { - class: `tiny-grid__empty-block${tableData.length ? '' : ' is__visible'} ${isCenterCls}`, - ref: 'emptyBlock' - }, - $slots.empty ? $slots.empty.call(_vm, { $table }, h) : renderEmpty ? [renderEmpty(h, $table)] : renderDefEmpty(h) - ) -} + hooks.watch(customFooter, (customFooter) => customFooter && resizeObserver.observe(customFooter)) -function renderBorders({ keyboardConfig, mouseConfig }) { - let res: any = null + hooks.watchEffect(() => { + if ($table.isColumnWidthAssigned) { + const columns = $table.scrollXLoad ? $table.visibleColumn : $table.tableColumn + const scrollWidth = columns.reduce((total, col) => (total += col.renderWidth), 0) + const clientWidth = bodyClientWidth.value - // 如果用户配置了鼠标和键盘配置项 - if (mouseConfig.checked || keyboardConfig.isCut) { - res = h('div', { class: 'tiny-grid__borders' }, [ - mouseConfig.checked ? renderBorder(h, 'check') : null, - keyboardConfig.isCut ? renderBorder(h, 'copy') : null - ]) - } + $table.horizonScroll.max = scrollWidth > clientWidth ? scrollWidth - clientWidth : 0 + } + }) - return res -} + hooks.onMounted(() => { + // 节流滚动降低滚动事件处理次数 + vm._throttleScrollHandler = throttle($table.optimizeOpts.scrollDelay, vm.handleScroll) -function renderTable({ $table, _vm, tableColumn, tableData, tableLayout }) { - return h( - 'table', - { - class: 'tiny-grid__body', - style: { tableLayout }, - attrs: { cellspacing: 0, cellpadding: 0, border: 0 }, - ref: 'table' - }, - [ - // 渲染colgroup标签,设置表格列宽度,保证表头的表格和表体的表格每列宽相同 - h( - 'colgroup', - { ref: 'colgroup' }, - tableColumn.map((column, columnIndex) => h('col', { attrs: { name: column.id }, key: columnIndex })) - ), - // 表格每次数据改变都会触发renderRow重新执行,会造成性能损失,此处待优化 - h('tbody', { ref: 'tbody' }, renderRows({ h, _vm, $table, $seq: '', rowLevel: 0, tableData, tableColumn })) - ] - ) -} + body.value?.addEventListener('scroll', vm._throttleScrollHandler) -// 如果scrollLoad存在,标识开启了滚动分页功能 -function renderYSpace({ scrollLoad }) { - return h('div', { class: 'tiny-grid-body__y-space visual', ref: 'ySpace' }, [ - scrollLoad ? h('div', { class: 'tiny-grid-body__y-scrollbar' }) : [null] - ]) -} + // 初始化行列拖拽 + setTimeout(() => { + const { dropConfig } = $table + + if (dropConfig) { + const { plugin, row = true, column = true, scheme } = dropConfig + + plugin && row && (vm.rowSortable = $table.rowDrop(body.value)) + + if (scheme !== 'v2') { + plugin && column && (vm.columnSortable = $table.columnDrop(body.value)) + } + } + }, 50) + }) -export default defineComponent({ - name: `${$prefix}GridBody`, - props: { - collectColumn: Array, - fixedColumn: Array, - isGroup: Boolean, - size: String, - tableColumn: Array, - tableData: Array, - visibleColumn: Array - }, - mounted() { - const { $el, $parent: $table, $refs } = this as any - const { elemStore, dropConfig } = $table - const keyPrefix = 'main-body-' - - // 表体第一层div,出现滚动条的dom元素 - elemStore[`${keyPrefix}wrapper`] = $el - // 表体table元素 - elemStore[`${keyPrefix}table`] = $refs.table - // colgroup元素,保持表头和表体宽度保持一致 - elemStore[`${keyPrefix}colgroup`] = $refs.colgroup - // tbody元素 - elemStore[`${keyPrefix}list`] = $refs.tbody - // x轴滚动条占位元素 - elemStore[`${keyPrefix}xSpace`] = $refs.xSpace - // y轴滚动条占位元素 - elemStore[`${keyPrefix}ySpace`] = $refs.ySpace - // 空数据元素 - elemStore[`${keyPrefix}emptyBlock`] = $refs.emptyBlock - - if (dropConfig) { - const { plugin, row = true } = dropConfig - plugin && row && (this.rowSortable = $table.rowDrop(this.$el)) - } - }, - beforeUnmount() { - this.rowSortable && this.rowSortable.destroy() - }, - updated() { - const { $parent: $table, fixedType } = this - !fixedType && $table.updateTableBodyHeight() - }, - setup(props, { slots }) { hooks.onBeforeUnmount(() => { - const table = hooks.getCurrentInstance().proxy + const { rowSortable, columnSortable } = vm - table.$el._onscroll = null - table.$el.onscroll = null + body.value?.removeEventListener('scroll', vm._throttleScrollHandler) + vm._throttleScrollHandler = null + rowSortable && rowSortable.destroy() + columnSortable && columnSortable.destroy() + resizeObserver.disconnect() }) - return { slots } + return { + slots, + rowHeight, + headerTable, + columnPool, + rowPool, + isNoData, + wrapperScrollLeft, + wrapperScrollTop, + stickyWrapper, + table, + body, + customFooter, + colgroup, + thead, + tbody, + ySpace, + normalRows, + footerRows + } }, render() { - let { $parent: $table } = this as any - let { $grid, isCenterEmpty, keyboardConfig = {}, mouseConfig = {}, renderEmpty } = $table - let { scrollLoad, tableColumn, tableData, tableLayout } = $table - let $slots = $grid.slots - let isCenterCls = isCenterEmpty ? 'is__center' : '' - - return h( - 'div', - { - ref: 'body', - class: ['tiny-grid__body-wrapper', 'body__wrapper', { [classMap.isScrollload]: scrollLoad }], - on: { - scroll: this.scrollEvent - } - }, - [ - // 表格主体内容x轴方向虚拟滚动条占位元素 - h('div', { class: 'tiny-grid-body__x-space', ref: 'xSpace' }), - renderYSpace({ scrollLoad }), - renderTable({ $table, _vm: this, tableColumn, tableData, tableLayout }), - // 开启鼠标或者配置项选中边框线 - renderBorders({ keyboardConfig, mouseConfig }), - // 空数据 - renderEmptyBlock({ $slots, $table, _vm: this, isCenterCls, renderEmpty, tableData }) - ] + const { $parent: $table, isNoData, tableColumn, footerData } = this + const { $grid, keyboardConfig, mouseConfig, scrollLoad, showFooter, renderFooter } = $table + const { containerScrollWidth, containerScrollHeight, scrollLoadScrollHeight } = $table + const { bodyWrapperHeight, bodyWrapperMinHeight, bodyWrapperMaxHeight } = $table + const $slots = $grid.slots + const _vm = this + + return ( +
+ {[ +
, +
+ {scrollLoad ? ( +
+ ) : null} +
, + // 内容表格 + renderTable({ $table, _vm }), + // 渲染自定义表尾 + showFooter && !isNoData && typeof renderFooter === 'function' ? ( + + ) : null, + // 选中边框线 + mouseConfig.checked || keyboardConfig.isCut ? ( +
+ {[mouseConfig.checked ? renderBorder('check') : null, keyboardConfig.isCut ? renderBorder('copy') : null]} +
+ ) : null, + // 空数据 + isNoData ? ( +
+ {$slots.empty + ? $slots.empty.call(_vm, { $table }, h) + : $table.renderEmpty + ? [$table.renderEmpty(h, $table)] + : [ + , + {GlobalConfig.i18n('ui.grid.emptyText')} + ]} +
+ ) : null + ]} +
) }, methods: { - // 滚动处理,如果存在列固定右侧,同步更新滚动状态 - scrollEvent(event) { - let { $parent: $table } = this as any - let { $refs, lastScrollLeft, lastScrollTop, scrollXLoad, scrollYLoad, columnStore } = $table - let { leftList, rightList } = columnStore - let { tableBody, tableFooter, tableHeader } = $refs - - // 获取主表头,主表体,主表尾,左表体,右表体 - let headerElem = tableHeader ? tableHeader.$el : null - let bodyElem = tableBody.$el - let footerElem = tableFooter ? tableFooter.$el : null - - // 获取主表体元素的滚动位置 - let scrollLeft = bodyElem.scrollLeft - let scrollTop = bodyElem.scrollTop - - // 对比当前滚动位置和最后一次滚动位置,来得到当前滚动的是哪个方向上的滚动条 - let isY = scrollTop !== lastScrollTop - let isX = scrollLeft !== lastScrollLeft - - // 记录新的滚动位置和时间 + handleScroll(event) { + const { $parent: $table, $el }: any = this + const { lastScrollLeft, lastScrollTop, scrollXLoad, scrollYLoad, horizonScroll } = $table + const { max, threshold } = horizonScroll + const { scrollLeft, scrollTop } = $el + const isX = scrollLeft !== lastScrollLeft + const isY = scrollTop !== lastScrollTop + + this.wrapperScrollLeft = scrollLeft + this.wrapperScrollTop = scrollTop + $table.lastScrollTime = Date.now() $table.lastScrollLeft = scrollLeft $table.lastScrollTop = scrollTop $table.scrollDirection = isX ? 'X' : 'Y' + $table.horizonScroll.isLeft = scrollLeft < threshold + $table.horizonScroll.isRight = scrollLeft > max - threshold - // 同步滚动条状态,只同步表头(表尾)滚动条状态,冻结列已优化为sticky方式 - syncHeaderAndFooterScroll({ bodyElem, footerElem, headerElem, isX }) - - // 处理关于冻结列最外层div类名 - if (leftList.length || rightList.length) { - generateFixedClassName({ $table, bodyElem, leftList, rightList }) + if (isX && scrollXLoad) { + $table.triggerScrollXEvent(event) } - // 处理x和y轴方法虚拟滚动数据加载逻辑 - doScrollLoad({ $table, _vm: this, bodyElem, event, headerElem, isX, isY, scrollLeft, scrollXLoad, scrollYLoad }) + if (isY && scrollYLoad) { + $table.triggerScrollYEvent(event) + } - // 触发用户监听的表格滚动事件 emitEvent($table, 'scroll', [{ type: 'body', scrollTop, scrollLeft, isX, isY, $table }, event]) + }, + resizeMousedown(event, params) { + let { $el, $parent: $table }: any = this + let { clientX: dragClientX, target: dragBtnElem } = event + let { column } = params + let { dragLeft = 0, minInterval = 40 } = {} + let { resizeBar: resizeBarElem, tableBody } = $table.$refs + let { cell = dragBtnElem.parentNode, dragBtnWidth = dragBtnElem.clientWidth } = {} + let { pos = getOffsetPos(dragBtnElem, $el), tableBodyElem = tableBody.$el } = {} + let dragMinLeft = pos.left - cell.clientWidth + dragBtnWidth + minInterval + let dragPosLeft = pos.left + Math.floor(dragBtnWidth) + let { oldMousemove = document.onmousemove, oldMouseup = document.onmouseup } = {} + + // 处理拖动事件 + let handleMousemoveEvent = function (event) { + event.stopPropagation() + event.preventDefault() + + let { offsetX = event.clientX - dragClientX, left = offsetX + dragPosLeft } = {} + let scrollLeft = tableBodyElem.scrollLeft + + dragLeft = Math.max(left, dragMinLeft) + if ($table.resizableConfig?.limit) { + const limitWidth = $table.resizableConfig?.limit({ + field: column.own.field, + width: column.renderWidth + (dragLeft - dragPosLeft) + }) + dragLeft = dragMinLeft - minInterval + limitWidth + } + + resizeBarElem.style.left = dragLeft - scrollLeft + 'px' + } + + resizeBarElem.style.display = 'block' + addClass($table.$el, 'tiny-grid-cell__resize') + $table._isResize = true + document.onmousemove = handleMousemoveEvent + + document.onmouseup = function () { + document.onmousemove = oldMousemove + document.onmouseup = oldMouseup + + let resizeWidth = column.renderWidth + (dragLeft - dragPosLeft) + + resizeWidth = typeof resizeWidth === 'number' ? resizeWidth : parseInt(resizeWidth, 10) || 40 + column.resizeWidth = resizeWidth < 40 ? 40 : resizeWidth + + resizeBarElem.style.display = 'none' + removeClass($table.$el, 'tiny-grid-cell__resize') + Object.assign($table, { _isResize: false, _lastResizeTime: Date.now() }) + $table.analyColumnWidth() + $table.recalculate() + + const toolbarVm = $table.getVm('toolbar') + + if (toolbarVm) { + toolbarVm.updateResizable() + } + + emitEvent($table, 'resizable-change', [params]) + + // 拖拽列宽后更新水平滚动最大位置 + if ($table.horizonScroll.fixed) { + setTimeout(() => { + let { scrollWidth, clientWidth, scrollLeft } = $table.$refs.tableBody.$el + + $table.horizonScroll.max = scrollWidth > clientWidth ? scrollWidth - clientWidth : 0 + $table.horizonScroll.isRight = scrollLeft > $table.horizonScroll.max - $table.horizonScroll.threshold + }, 50) + } + } + + handleMousemoveEvent(event) + }, + handleScrollLoad(e) { + const { $parent: $table } = this + if ($table.scrollLoad) { + $table.debounceScrollLoad(e) + } } } }) diff --git a/packages/vue/src/grid/src/body/src/usePool.ts b/packages/vue/src/grid/src/body/src/usePool.ts new file mode 100644 index 0000000000..93e07b2fd4 --- /dev/null +++ b/packages/vue/src/grid/src/body/src/usePool.ts @@ -0,0 +1,108 @@ +import { hooks } from '@opentiny/vue-common' + +const difference = (arr, other) => arr.filter((i) => other.findIndex((j) => i.id === j.id) === -1) + +let uid = 0 + +const createPool = (array) => { + if (!Array.isArray(array)) { + return + } + + const context = { + pool: [], + idViewMap: new Map(), + unusedViews: [], + array + } + + array.forEach((item) => { + const view = { id: ++uid, used: true, item } + context.pool.push(view) + context.idViewMap.set(item.id, view) + }) + + return context +} + +const updatePool = (array, context) => { + if (!Array.isArray(array)) { + return + } + + const expires = difference(context.array, array) + const indices = new WeakMap() + + expires.forEach((item) => { + const view = context.idViewMap.get(item.id) + + view.used = false + + context.idViewMap.delete(item.id) + context.unusedViews.push(view) + }) + + array.forEach((item, i) => { + indices.set(item, i) + + let view = context.idViewMap.get(item.id) + + if (!view) { + if (context.unusedViews.length > 0) { + view = context.unusedViews.shift() + } else { + view = { id: ++uid, used: true, item } + context.pool.push(view) + } + + context.idViewMap.set(item.id, view) + } + + view.used = true + view.item = item + }) + + context.array = array + context.pool.sort((a, b) => (a.used ? (b.used ? indices.get(a.item) - indices.get(b.item) : -1) : b.used ? 1 : 0)) + + return context +} + +export const usePool = (props) => { + const columnPool = hooks.ref([]) + const rowPool = hooks.ref([]) + const isNoData = hooks.ref(true) + + let columnContext + + hooks.watch( + () => props.tableColumn, + () => { + if (columnContext) { + updatePool(props.tableColumn, columnContext) + } else { + columnContext = createPool(props.tableColumn) + } + + columnPool.value = columnContext.pool + } + ) + + let rowContext + + hooks.watch( + () => props.tableNode, + () => { + if (rowContext) { + updatePool(props.tableNode, rowContext) + } else { + rowContext = createPool(props.tableNode) + } + + rowPool.value = rowContext?.pool || rowPool.value + isNoData.value = !(props.tableNode?.length > 0) + } + ) + + return { columnPool, rowPool, isNoData } +} diff --git a/packages/vue/src/grid/src/cell/src/cell.ts b/packages/vue/src/grid/src/cell/src/cell.ts index ccf1809d7d..ab10749d61 100644 --- a/packages/vue/src/grid/src/cell/src/cell.ts +++ b/packages/vue/src/grid/src/cell/src/cell.ts @@ -394,16 +394,16 @@ export const Cell = { return Cell.renderTreeIcon(h, params).concat(Cell.renderIndexCell(h, params)) }, renderIndexCell(h, params) { - const { $table, column, row, seq, $seq, level } = params + const { $table, column, row, seq, level } = params // startIndex:序号列的起始值 - const { startIndex, treeConfig, scrollYLoad, treeOrdered } = $table + const { startIndex, treeConfig, treeOrdered } = $table const { indexMethod, slots } = column const { temporaryIndex = '_$index_' } = treeConfig || {} const isTreeOrderedFalse = treeConfig && !treeOrdered let indexValue = startIndex + seq // tree-config为false的情况下,序号为1.1这种形式 if (isTreeOrderedFalse && level) { - indexValue = scrollYLoad ? row[temporaryIndex] : `${$seq}.${seq}` + indexValue = row[temporaryIndex] } if (slots && slots.default) { diff --git a/packages/vue/src/grid/src/checkbox/src/methods.ts b/packages/vue/src/grid/src/checkbox/src/methods.ts index c00058f7a2..4387cd5737 100644 --- a/packages/vue/src/grid/src/checkbox/src/methods.ts +++ b/packages/vue/src/grid/src/checkbox/src/methods.ts @@ -1,7 +1,6 @@ import { hasCheckField, hasNoCheckField } from './handleSelectRow' import { hasCheckFieldNoStrictly, hasNoCheckFieldNoStrictly, setSelectionNoStrictly } from './setAllSelection' -import { getTableRowKey } from '../../table/src/strategy' -import { emitEvent } from '@opentiny/vue-renderless/grid/utils' +import { emitEvent, getRowkey } from '@opentiny/vue-renderless/grid/utils' import { isArray, set, get, eachTree, find, toStringJSON, toArray } from '@opentiny/vue-renderless/grid/static/' export default { @@ -129,7 +128,7 @@ export default { reserveCheckSelection() { let { fullDataRowIdData, selection } = this let { reserve } = this.selectConfig || {} - let rowkey = getTableRowKey(this) + let rowkey = getRowkey(this) if (reserve && selection.length) { this.selection = selection.map((row) => { let rowCache = fullDataRowIdData[`${get(row, rowkey)}`] @@ -215,14 +214,15 @@ export default { let selected = this.getSelectRecords() let position = typeof selectToolbar === 'object' ? selectToolbar.position : '' if (selectColumn && selected && selected.length) { - let selectTh = this.$el.querySelector('th.tiny-grid-header__column.col__selection') - let headerWrapper = this.$el.querySelector('.tiny-grid>.tiny-grid__header-wrapper') + const { tinyTheme, vSize, $el } = this + const rowHeight = GlobalConfig.rowHeight[tinyTheme]?.[vSize || 'default'] || 40 + let selectTh = $el.querySelector('th.tiny-grid-header__column.col__selection') let tr = selectTh.parentNode let thArr = toArray(tr.childNodes) let range = document.createRange() let rangeBoundingRect - let headerBoundingRect = headerWrapper.getBoundingClientRect() - let layout = { width: 0, height: 0, left: 0, top: 0, zIndex: 1 } + let headerBoundingRect = { width: $el.getBoundingClientRect().width, height: rowHeight } + let layout = { width: 0, height: 0, left: 0, top: 0, zIndex: 20 } let adjust = 1 if (selectColumn.fixed === 'right') { range.setStart(tr, thArr.indexOf(selectTh)) diff --git a/packages/vue/src/grid/src/column-anchor/src/methods.ts b/packages/vue/src/grid/src/column-anchor/src/methods.ts index 8d833f486f..d6bcfd3518 100644 --- a/packages/vue/src/grid/src/column-anchor/src/methods.ts +++ b/packages/vue/src/grid/src/column-anchor/src/methods.ts @@ -91,7 +91,11 @@ export default { activeAnchor, action: (field, e) => this.anchorAction({ field, anchors, _vm: this, e }) } - this.emitter.once('active-anchor', () => this.anchorAction({ field: activeAnchor.field, anchors, _vm: this })) + + this._delayActivateAnchor = () => { + this._delayActivateAnchor = undefined + setTimeout(() => this.anchorAction({ field: activeAnchor.field, anchors, _vm: this }), activeAnchor.delay) + } }, anchorAction({ field, anchors, _vm }) { const fromAnchor = anchors.find((anchor) => anchor.active) diff --git a/packages/vue/src/grid/src/column/src/column.ts b/packages/vue/src/grid/src/column/src/column.ts index 775313249e..279bcadeb0 100644 --- a/packages/vue/src/grid/src/column/src/column.ts +++ b/packages/vue/src/grid/src/column/src/column.ts @@ -23,7 +23,6 @@ * */ import { findTree } from '@opentiny/vue-renderless/grid/static' -import { setColumnFormat } from '@opentiny/vue-renderless/grid/utils' import { h, hooks, $props, defineComponent, useRelation, useInstanceSlots } from '@opentiny/vue-common' import Cell from '../../cell' import { warn } from '../../tools' @@ -162,7 +161,7 @@ export default defineComponent({ watch( () => props.formatConfig, - () => setColumnFormat(state.columnConfig, props) + () => (state.columnConfig.format = props.formatConfig) ) onUpdated(() => { diff --git a/packages/vue/src/grid/src/composable/index.ts b/packages/vue/src/grid/src/composable/index.ts index bd25c3d097..b46e748753 100644 --- a/packages/vue/src/grid/src/composable/index.ts +++ b/packages/vue/src/grid/src/composable/index.ts @@ -1,2 +1,7 @@ export * from './useDrag' export * from './useRowGroup' +export * from './useCellStatus' +export * from './useData' +export * from './useHeader' +export * from './useCellEvent' +export * from './useCellSpan' diff --git a/packages/vue/src/grid/src/composable/useCellEvent.ts b/packages/vue/src/grid/src/composable/useCellEvent.ts new file mode 100644 index 0000000000..683c6b97a8 --- /dev/null +++ b/packages/vue/src/grid/src/composable/useCellEvent.ts @@ -0,0 +1,365 @@ +/* eslint-disable no-cond-assign */ +import { hooks } from '@opentiny/vue-common' +import { off, on, isNull } from '@opentiny/utils' +import { isUndefined } from '@opentiny/vue-renderless/grid/static/' +import { updateCellTitle, emitEvent } from '@opentiny/vue-renderless/grid/utils' + +const getEventSource = (e, $table) => { + const target = e.target + const tableEl = target.closest('.tiny-grid__body') + + if (tableEl.dataset?.tableid !== String($table.id)) return + + let cellEl = target.closest('.tiny-grid-header__column') + let rowEl, part, rowType, row, column + + if (cellEl) { + rowEl = cellEl.parentNode + part = 'header' + } else if ((cellEl = target.closest('.tiny-grid-body__column'))) { + rowEl = cellEl.parentNode + part = 'body' + } else if ((cellEl = target.closest('.tiny-grid-footer__column'))) { + rowEl = cellEl.parentNode + part = 'footer' + } + + if (!part || !cellEl || !rowEl) return + + column = $table.getColumnNode(cellEl)?.item + + if (rowEl.dataset?.rowid?.startsWith('row_g_')) { + rowType = 'virtual' + } else if (part === 'body') { + rowType = 'normal' + row = $table.getRowNode(rowEl)?.item + } + + return { part, rowType, row, column, cell: cellEl, tr: rowEl } +} + +const parseNumber = (numStr) => (isUndefined(numStr) ? numStr : parseInt(numStr, 10)) + +const normalOverflow = (overflow, _overflow) => { + overflow = isNull(overflow) ? _overflow : overflow + + const overflowTitle = overflow === 'title' + const overflowTooltip = overflow === true || overflow === 'tooltip' + const overflowEllipsis = overflow === 'ellipsis' + const overflowHint = overflowTitle || overflowTooltip || overflowEllipsis + + return { overflow, overflowTitle, overflowTooltip, overflowEllipsis, overflowHint } +} + +export const getConfigOverflow = (column, $table) => { + const { showOverflow: _cellOverflow, showHeaderOverflow: _headerOverflow } = $table + const { showTip, showOverflow, showHeaderTip, showHeaderOverflow } = column || {} + + const { + overflowTitle: cellOverflowTitle, + overflowTooltip: cellOverflowTooltip, + overflowEllipsis: cellOverflowEllipsis, + overflowHint: cellOverflowHint + } = normalOverflow(showOverflow, _cellOverflow) + + const { + overflowTitle: headerOverflowTitle, + overflowTooltip: headerOverflowTooltip, + overflowEllipsis: headerOverflowEllipsis, + overflowHint: headerOverflowHint + } = normalOverflow(showHeaderOverflow, _headerOverflow) + + return { + cellTip: showTip, + cellOverflowTitle, + cellOverflowTooltip, + cellOverflowEllipsis, + cellOverflowHint, + headerTip: showHeaderTip, + headerOverflowTitle, + headerOverflowTooltip, + headerOverflowEllipsis, + headerOverflowHint + } +} + +const getEventParams = ({ row, column, cell, tr }, $table) => { + const { showTip, showHeaderTip } = column || {} + const rowIndex = $table.getRowIndex(row) + const columnIndex = $table.getColumnIndex(column) + const { seq, colindex } = cell?.dataset || {} + const { rowindex, rowlevel } = tr?.dataset || {} + + return { + $table, + row, + column, + cell, + rowIndex, + columnIndex, + showTip, + showHeaderTip, + seq: parseNumber(seq), + $rowIndex: parseNumber(rowindex), + $columnIndex: parseNumber(colindex), + level: parseNumber(rowlevel) + } +} + +// 滚动、拖动过程中不需要触发 +const isOperateMouse = ($table) => { + return ( + $table._isResize || ($table.lastScrollTime && Date.now() < $table.lastScrollTime + $table.optimizeOpts.delayHover) + ) +} + +export const useCellEvent = ({ table, $table }) => { + let isBound = false + const hoverCell = hooks.shallowRef() + const hoverCellContext = hooks.shallowRef() + const hoverRow = hooks.shallowRef() + const hoverText = hooks.shallowRef() + let textContent + + const handleMouseEnter = (e) => { + if (isOperateMouse($table)) return + const source = getEventSource(e, $table) + + if (source) { + const params = getEventParams(source, $table) + + if (hoverCell.value !== source.cell) { + hoverCell.value = source.cell + hoverCellContext.value = { params, source, e } + textContent = + hoverCell.value.querySelector('.tiny-grid-cell-text') || hoverCell.value.querySelector('.tiny-grid-cell') + } + + if (e.target === textContent) { + hoverText.value = textContent + } + + if (source.part !== 'body') { + hoverRow.value = null + } else if (hoverRow.value !== source.row) { + hoverRow.value = source.row + } + } + } + + const handleMouseLeave = (e) => { + if (isOperateMouse($table)) return + + const target = e.target + + if (target === hoverText.value) { + hoverText.value = null + } + + if (target.localName === 'table' && target.dataset?.tableid === String($table.id)) { + hoverRow.value = hoverCell.value = hoverCellContext.value = null + } + } + + const handleMouseDown = (e) => { + const source = getEventSource(e, $table) + + if (source) { + const params = getEventParams(source, $table) + const { mouseConfig = {} } = $table + + if (source.part === 'header' && mouseConfig.checked) { + $table.triggerHeaderCellMousedownEvent(e, params) + } + + if (source.part === 'body' && (mouseConfig.checked || mouseConfig.selected)) { + $table.triggerCellMousedownEvent(e, params) + } + } + } + + const handleClick = (e) => { + const source = getEventSource(e, $table) + + if (source) { + const params = getEventParams(source, $table) + const column = source.column || {} + const { editor } = column + const satisfy = (equal, trigger) => trigger === 'row' || (equal(column) && trigger === 'cell') + const { + editConfig, + expandConfig = {}, + highlightCurrentColumn, + highlightCurrentRow, + mouseConfig = {}, + radioConfig = {}, + selectConfig = {}, + sortOpts, + tableListeners, + treeConfig = {} + } = $table + + if ( + source.part === 'header' && + (highlightCurrentColumn || + tableListeners['header-cell-click'] || + mouseConfig.checked || + sortOpts.trigger === 'cell') + ) { + $table.triggerHeaderCellClickEvent(e, params) + } + + if ( + source.part === 'body' && + (highlightCurrentRow || + tableListeners['cell-click'] || + mouseConfig.checked || + (editor && editConfig) || + satisfy(() => true, expandConfig.trigger) || + satisfy(({ type }) => type === 'radio', radioConfig.trigger) || + satisfy(({ type }) => type === 'selection', selectConfig.trigger) || + satisfy(({ treeNode }) => treeNode, treeConfig.trigger)) + ) { + $table.triggerCellClickEvent(e, params) + } + + if (source.part === 'footer' && tableListeners['footer-cell-click']) { + emitEvent($table, 'footer-cell-click', [params, e]) + } + } + } + + const handleDoubleClick = (e) => { + const source = getEventSource(e, $table) + + if (source) { + const params = getEventParams(source, $table) + const column = source.column || {} + const { editor } = column + const { editConfig, tableListeners } = $table + const triggerDblclick = editor && editConfig && editConfig.trigger === 'dblclick' + + if (source.part === 'header' && tableListeners['header-cell-dblclick']) { + emitEvent($table, 'header-cell-dblclick', [params, e]) + } + + if (source.part === 'body' && (triggerDblclick || tableListeners['cell-dblclick'])) { + $table.triggerCellDBLClickEvent(e, params) + } + + if (source.part === 'footer' && tableListeners['footer-cell-dblclick']) { + emitEvent($table, 'footer-cell-dblclick', [params, e]) + } + } + } + + const bindMouseEvents = (target) => { + on(target, 'mouseenter', handleMouseEnter, true) + on(target, 'mouseleave', handleMouseLeave, true) + on(target, 'mousedown', handleMouseDown, true) + on(target, 'click', handleClick, true) + on(target, 'dblclick', handleDoubleClick, true) + } + + const unbindMouseEvents = (target) => { + off(target, 'mouseenter', handleMouseEnter, true) + off(target, 'mouseleave', handleMouseLeave, true) + off(target, 'mousedown', handleMouseDown, true) + off(target, 'click', handleClick, true) + off(target, 'dblclick', handleDoubleClick, true) + } + + hooks.onBeforeUnmount(() => { + if (isBound && table.value) { + unbindMouseEvents(table.value) + isBound = false + hoverRow.value = hoverCell.value = hoverCellContext.value = null + $table.hoverCell = null + } + }) + + hooks.watch(table, (table, old) => { + if (isBound && old) { + unbindMouseEvents(old) + isBound = false + } + + if (!isBound && table) { + bindMouseEvents(table) + isBound = true + } + }) + + hooks.watch(hoverText, (curText, preText) => { + const { params, source, e } = hoverCellContext.value + const column = source.column || {} + const { headerOverflowTitle, headerOverflowTooltip, headerTip, cellOverflowTitle, cellOverflowTooltip, cellTip } = + getConfigOverflow(column, $table) + + $table.hoverText = curText + + if (preText) { + if (source.part === 'header') { + if (headerTip || headerOverflowTooltip) { + $table.clostTooltip() + } + } else if (source.part === 'body') { + if (cellTip || cellOverflowTooltip) { + $table.clostTooltip() + } + } else if (source.part === 'footer') { + if (cellOverflowTooltip) { + $table.clostTooltip() + } + } + } + + if (curText) { + if (source.part === 'header') { + if (headerOverflowTitle) { + updateCellTitle(e, source.cell) + } else if (headerTip || headerOverflowTooltip) { + $table.triggerHeaderTooltipEvent(e, params) + } + } else if (source.part === 'body') { + if (cellOverflowTitle) { + updateCellTitle(e, source.cell) + } else if (cellTip || cellOverflowTooltip) { + $table.triggerTooltipEvent(e, params) + } + } else if (source.part === 'footer') { + if (cellOverflowTitle) { + updateCellTitle(e, source.cell) + } else if (cellOverflowTooltip) { + $table.triggerFooterTooltipEvent(e, params) + } + } + } + }) + + hooks.watch([hoverCell, hoverCellContext], ([curCell, curCtx], [preCell, preCtx]) => { + const { tableListeners } = $table + + if (preCell) { + const { params, source, e } = preCtx + + if (source.part === 'body') { + if (tableListeners['cell-mouseleave']) { + emitEvent($table, 'cell-mouseleave', [params, e]) + } + } + } + + if (curCell) { + const { params, source, e } = curCtx + + $table.hoverCell = curCell + + if (source.part === 'body') { + if (tableListeners['cell-mouseenter']) { + emitEvent($table, 'cell-mouseenter', [params, e]) + } + } + } + }) +} diff --git a/packages/vue/src/grid/src/composable/useCellSpan.ts b/packages/vue/src/grid/src/composable/useCellSpan.ts new file mode 100644 index 0000000000..344d3071fb --- /dev/null +++ b/packages/vue/src/grid/src/composable/useCellSpan.ts @@ -0,0 +1,245 @@ +import { hooks } from '@opentiny/vue-common' +import { isVirtualRow } from '../table/src/strategy' +import { getRowid } from '@opentiny/vue-renderless/grid/utils' + +export const useCellSpan = (bodyVm, bodyProps) => { + const $table = bodyVm.$parent + const normalRows = hooks.shallowRef({}) + const footerRows = hooks.shallowRef({}) + + hooks.watch( + [() => $table.visibleColumn, () => bodyProps.tableData, () => $table.isColumnWidthAssigned], + ([visibleColumn, tableData, isColumnWidthAssigned]) => { + if (!Array.isArray(tableData) || !isColumnWidthAssigned) { + return + } + + const { tableNode } = bodyProps + const { hasVirtualRow, treeConfig, treeOrdered, scrollYLoad, scrollYStore, afterFullData, columnStore } = $table + const { hideMethod } = treeConfig || {} + const isOrdered = treeConfig ? !!treeOrdered : false + const seqCount = { value: 0 } + const startIndex = scrollYStore.startIndex + const { leftList, rightList } = columnStore + + const normalState = {} + const normalTable = [] + + for (let $rowIndex = 0; $rowIndex < tableData.length; $rowIndex++) { + const row = tableData[$rowIndex] + const rowLevel = tableNode[$rowIndex].level + const rowIndex = $table.getRowIndex(row) + let virtualRow = false + + if (hasVirtualRow) { + virtualRow = isVirtualRow(row) + } + + const isSkipRowRender = (hideMethod && hideMethod(row, rowLevel)) || virtualRow + + if (!isSkipRowRender) { + seqCount.value = seqCount.value + 1 + } + + let seq = isOrdered ? seqCount.value : $rowIndex + 1 + + if (scrollYLoad) { + seq += startIndex + } + + if (hasVirtualRow) { + if (virtualRow) { + // 分组行列合并的详细计算逻辑在useRowGroup.ts + continue + } else { + seq = afterFullData.indexOf(row) + 1 + } + } + + const params = { $table, data: tableData, row, $rowIndex, rowIndex, level: rowLevel, $seq: '', seq } + + stateNormalCell(normalState, normalTable, params, $table) + } + + if (leftList.length > 0) { + adjustColspan(normalTable, leftList.length - 1, true) + } + + if (rightList.length > 0) { + adjustColspan(normalTable, visibleColumn.indexOf(rightList[0]) - 1, false) + } + + normalRows.value = normalState + } + ) + + hooks.watch( + [() => $table.visibleColumn, () => bodyProps.footerData, () => $table.isColumnWidthAssigned], + ([visibleColumn, footerData, isColumnWidthAssigned]) => { + if (!Array.isArray(footerData) || !isColumnWidthAssigned) { + return + } + + const { columnStore } = $table + const { leftList, rightList } = columnStore + + const footerState = {} + const footerTable = [] + + for (let $rowIndex = 0; $rowIndex < footerData.length; $rowIndex++) { + const params = { $table, $rowIndex, data: footerData } + stateFooterCell(footerState, footerTable, params, $table) + } + + if (leftList.length > 0) { + adjustColspan(footerTable, leftList.length - 1, true) + } + + if (rightList.length > 0) { + adjustColspan(footerTable, visibleColumn.indexOf(rightList[0]) - 1, false) + } + + footerRows.value = footerState + } + ) + + return { normalRows, footerRows } +} + +const stateNormalCell = (state, table, params, $table) => { + const { rowSpan, spanMethod, visibleColumn } = $table + const rowId = getRowid($table, params.row) + const rowState = {} + const rowAttrs = [] + + for (let $columnIndex = 0; $columnIndex < visibleColumn.length; $columnIndex++) { + const column = visibleColumn[$columnIndex] + const columnIndex = $table.getColumnIndex(column) + const attrs = { rowspan: 1, colspan: 1, visible: true, _stickyClass: '', _stickyStyle: null } + + params = { ...params, column, $columnIndex, columnIndex } + + if (spanMethod || rowSpan) { + doSpan({ attrs, params, rowSpan, spanMethod }) + } + + rowState[column.id] = { attrs, params } + rowAttrs.push({ attrs, params }) + } + + state[rowId] = rowState + table.push(rowAttrs) +} + +const stateFooterCell = (state, table, params, $table) => { + const { footerSpanMethod, visibleColumn } = $table + const rowState = {} + const rowAttrs = [] + + for (let $columnIndex = 0; $columnIndex < visibleColumn.length; $columnIndex++) { + const column = visibleColumn[$columnIndex] + const columnIndex = $table.getColumnIndex(column) + const attrs = { rowspan: 1, colspan: 1, visible: true } + + params = { ...params, column, $columnIndex, columnIndex } + + if (footerSpanMethod) { + const { rowspan = 1, colspan = 1 } = footerSpanMethod(params) || {} + + attrs.rowspan = rowspan + attrs.colspan = colspan + attrs.visible = rowspan > 0 && colspan > 0 + } + + rowState[column.id] = { attrs, params } + rowAttrs.push({ attrs, params }) + } + + state[params.$rowIndex] = rowState + table.push(rowAttrs) +} + +const doSpan = ({ attrs, params, rowSpan, spanMethod }) => { + const { rowspan = 1, colspan = 1 } = (spanMethod ? spanMethod(params) : rowSpanMethod(rowSpan, params)) || {} + + attrs.rowspan = rowspan + attrs.colspan = colspan + attrs.visible = rowspan > 0 && colspan > 0 +} + +const rowSpanMethod = (rowSpan, { row, $rowIndex, column, data }) => { + const fields = [] + + if (column.visible && rowSpan) { + rowSpan.forEach((item) => fields.push(item.field)) + + const cellVal = row[column.property] + + if (cellVal && fields.includes(column.property)) { + const prevSiblingRow = data[$rowIndex - 1] + let nextSiblingRow = data[$rowIndex + 1] + + if (prevSiblingRow?.[column.property] === cellVal) { + return { rowspan: 0, colspan: 0 } + } else { + let rowspanCount = 1 + + while (nextSiblingRow?.[column.property] === cellVal) { + nextSiblingRow = data[++rowspanCount + $rowIndex] + } + + if (rowspanCount > 1) { + return { rowspan: rowspanCount, colspan: 1 } + } + } + } + } +} + +const adjustColspan = (table, pos, isLeft) => { + if (pos < 0) { + return + } + + for (let i = 0; i < table.length; i++) { + const row = table[i] + + for (let j = 0; j <= pos && j < row.length; j++) { + const { attrs } = row[j] + const oldColspan = attrs.colspan + let k, posCol + + if (oldColspan > 1 && (k = j + oldColspan - 1) > pos) { + attrs.colspan = pos - j + 1 + + if (isLeft) { + attrs._stickyClass = 'fixed-left-last__column' + } + + if ((posCol = row[pos + 1])) { + posCol.attrs.colspan = k - pos + + if (posCol.attrs.rowspan < 1) { + posCol.attrs.rowspan = attrs.rowspan + } + + posCol.attrs.visible = true + } + } + } + + if (!isLeft) { + const rightCols = row.slice(pos + 1).reverse() + let right = 0 + + for (let j = 0; j < rightCols.length; j++) { + const { attrs, params } = rightCols[j] + + if (attrs.visible) { + attrs._stickyStyle = { right: `${right}px` } + right += params.column.renderWidth + } + } + } + } +} diff --git a/packages/vue/src/grid/src/composable/useCellStatus.ts b/packages/vue/src/grid/src/composable/useCellStatus.ts new file mode 100644 index 0000000000..da6ce2a63d --- /dev/null +++ b/packages/vue/src/grid/src/composable/useCellStatus.ts @@ -0,0 +1,50 @@ +import { getRowid } from '@opentiny/vue-renderless/grid/utils' + +const isCellDirty = ($table, row, column) => { + const { editConfig } = $table + const { showStatus = false, relationFields = true } = editConfig || {} + // 关联字段配置为true,或者配置包含当前字段时,支持脏数据检查 + const canChange = + relationFields === true || (Array.isArray(relationFields) && relationFields.includes(column.property)) + + let isDirty + + // 冻结表格方案:主表的固定隐藏列不进行脏数据检查。改为粘性布局后:主表的所有列都应去掉此限制。 + if (editConfig && showStatus && column.property && (column.editor || (relationFields && canChange))) { + isDirty = $table.hasRowChange(row, column.property) + } + + return isDirty +} + +export const getCellKey = ($table, row, column) => { + const rowid = getRowid($table, row) + return `${rowid}-${column.id}` +} + +const updateCellStatus = ($table, row, column) => { + const cellKey = getCellKey($table, row, column) + const isDirty = isCellDirty($table, row, column) + const map = $table.cellStatus + + if (map.has(cellKey)) { + map.get(cellKey).isDirty = isDirty + } else { + map.set(cellKey, { isDirty }) + } +} + +export const updateRowStatus = ($table, row) => { + $table.tableFullColumn.forEach((column) => updateCellStatus($table, row, column)) +} + +export const getCellStatus = ($table, row, column) => { + const cellKey = getCellKey($table, row, column) + const map = $table.cellStatus + + if (map.has(cellKey)) { + return map.get(cellKey) + } else { + return { isDirty: false } + } +} diff --git a/packages/vue/src/grid/src/composable/useData.ts b/packages/vue/src/grid/src/composable/useData.ts new file mode 100644 index 0000000000..cbf29c6e66 --- /dev/null +++ b/packages/vue/src/grid/src/composable/useData.ts @@ -0,0 +1,126 @@ +import { getRowid } from '@opentiny/vue-renderless/grid/utils' + +const isContented = (array) => Array.isArray(array) && array.length > 0 + +let nid = 0 + +const structure = ({ array, stack, tiled, map, customMappings, getID, childrenKey, sizeKey }) => { + if (!Array.isArray(array)) { + return + } + + const level = stack.length + const nodes = [] + + for (let i = 0; i < array.length; i++) { + const item = array[i] + + const node = { + id: getID(item) || ++nid, + payload: item, + path: [...stack, item], + level, + parentNode: level > 0 ? map.get(stack[stack.length - 1]) : undefined, + childNodes: undefined, + space: { originDistance: 0, size: item[sizeKey] || 36 }, + mappings: customMappings ? Object.assign({}, customMappings({ payload: item, viewIndex: tiled.length })) : {} + } + + tiled.push(node) + map.set(item, node) + nodes.push(node) + + if (childrenKey) { + stack.push(item) + node.childNodes = structure({ + array: item[childrenKey], + stack, + tiled, + map, + customMappings, + getID, + childrenKey, + sizeKey + }) + stack.pop() + } + } + + return nodes +} + +const makeTile = ({ list, getID, childrenKey, sizeKey, customMappings }) => { + const tiled = [] + const map = new WeakMap() + + if (isContented(list)) { + structure({ + array: list, + stack: [], + tiled, + map, + customMappings, + getID, + childrenKey, + sizeKey + }) + } + + return { tiled, map } +} + +const getAncestors = (node, map) => + node.parentNode ? node.path.slice(0, node.path.length - 1).map((row) => map.get(row)) : [] + +const isExpand = (node, isRowExpand) => isRowExpand(node.payload) + +const isParentExpand = (node, isRowExpand, map) => getAncestors(node, map).every((p) => isExpand(p, isRowExpand)) + +const makeGraph = ({ isRowExpand, tileInfo }) => { + const { tiled, map } = tileInfo + const graphed = [] + let scrollSize = 0 + + for (let i = 0; i < tiled.length; i++) { + const node = tiled[i] + + if (!node.parentNode || isParentExpand(node, isRowExpand, map)) { + node.space.originDistance = scrollSize + scrollSize += node.space.size + graphed.push(node) + } + } + + return { graphed, scrollSize } +} + +export const buildRenderGraph = ($table) => { + tileFullData($table) + graphFullData($table) +} + +export const tileFullData = ($table) => { + const { treeConfig, rowGroup, groupFullData, afterFullData } = $table + + // @ts-expect-error + const tileInfo = makeTile({ + list: !treeConfig && rowGroup?.field ? groupFullData : afterFullData, + getID: (row) => getRowid($table, row), + childrenKey: treeConfig ? treeConfig.children : undefined + }) + + $table._tileInfo = tileInfo +} + +export const graphFullData = ($table) => { + const { treeConfig, treeExpandeds, _tileInfo } = $table + + if (_tileInfo) { + const graphInfo = makeGraph({ + isRowExpand: (row) => (treeConfig ? treeExpandeds.includes(row) : true), + tileInfo: $table._tileInfo + }) + + $table._graphInfo = graphInfo + } +} diff --git a/packages/vue/src/grid/src/composable/useDrag/index.ts b/packages/vue/src/grid/src/composable/useDrag/index.ts index 25b1c8022f..d4ba114834 100644 --- a/packages/vue/src/grid/src/composable/useDrag/index.ts +++ b/packages/vue/src/grid/src/composable/useDrag/index.ts @@ -9,6 +9,8 @@ const headerTh = 'th.tiny-grid-header__column:not(.col__gutter):not(.fixed__hidd const groupKey = 'dndGroup' const idKey = 'colid' const pidKey = 'pColid' +let timer = null +const time = 2000 let dndGroup = 0 @@ -74,6 +76,9 @@ const getColidMap = (treeArray) => { } const createDragHander = (state, $table) => { + const dropConfig = state.dropConfig + const dropable = dropConfig?.column && dropConfig?.scheme === 'v2' + // 开始拖拽处理 const dragStart = (dragTarget) => { const dragColid = dragTarget.dataset.colid @@ -84,6 +89,7 @@ const createDragHander = (state, $table) => { const dragIndex = dragParentChildren.indexOf(dragColumn) $table.$emit('column-drag-start', { dragParentChildren, dragColumn, dragIndex }) + clearTimeout(timer) } // 放置结束处理 @@ -103,7 +109,7 @@ const createDragHander = (state, $table) => { // 拖拽信息参数 const args = { dragParentChildren, dragColumn, dragIndex, dropParentChildren, dropColumn, dropIndex } // 放置前处理 - callInterceptor(state.dropConfig.columnBeforeDrop, { + callInterceptor(state.dropConfig?.columnBeforeDrop, { args: [args], done: () => { // 移除被拖拽列,并插入到被放置的位置 @@ -123,6 +129,13 @@ const createDragHander = (state, $table) => { scrollYLoad && $table.triggerScrollYEvent({ target: { scrollTop: lastScrollTop } }) } }) + + // 2s后用户没有再次拖动排序再保存数据,防止事件频繁触发消耗性能 + if ($table.getVm('toolbar') && dropable) { + timer = setTimeout(() => { + $table.getVm('toolbar').$refs.custom.saveSettings('drag') + }, time) + } } }) } @@ -132,78 +145,67 @@ const createDragHander = (state, $table) => { const createTableColumnWatch = ($table, state, isColumnGroupLevel, stopHandlerMap) => debounce(100, () => { - const headers = ['table', 'left', 'right'] + const dndProxy = $table.$el.querySelector('thead') - headers.forEach((key) => { - const headerVm = $table.$refs[`${key}Header`] + if (dndProxy) { + let dndThs = dndProxy.querySelectorAll(headerTh) - if (headerVm) { - const dndProxy = headerVm.$el - - if (dndProxy) { - const dndThs = Array.from(dndProxy.querySelectorAll(headerTh)) + if (dndThs.length > 0) { + dndThs = Array.from(dndThs) + } - // 表头th渲染时已添加data-colid属性,这里额外增加draggable、data-p-colid和data-dnd-group属性 - setDndAttribute(dndThs, state.colidMap, isColumnGroupLevel) + // 表头th渲染时已添加data-colid属性,这里额外增加draggable、data-p-colid和data-dnd-group属性 + setDndAttribute(dndThs, state.colidMap, isColumnGroupLevel) - if (stopHandlerMap.has(dndProxy)) { - stopHandlerMap.get(dndProxy).destroy() - stopHandlerMap.delete(dndProxy) - } + if (stopHandlerMap.has(dndProxy)) { + stopHandlerMap.get(dndProxy).destroy() + stopHandlerMap.delete(dndProxy) + } - const { dragStart, drop } = createDragHander(state, $table) - const dropClass = state.dropConfig.columnDropClass || '' + const { dragStart, drop } = createDragHander(state, $table) + const dropClass = state.dropConfig?.columnDropClass || '' - stopHandlerMap.set( - dndProxy, - initDrag(dndProxy, dndThs, { dragStart, drop, dropClass, groupKey, idKey, pidKey }) - ) - } - } - }) + stopHandlerMap.set(dndProxy, initDrag(dndProxy, dndThs, { dragStart, drop, dropClass, groupKey, idKey, pidKey })) + } }) -const createUseDrag = - ({ reactive, watch, getCurrentInstance, onBeforeUnmount }) => - ({ dropConfig, collectColumn, tableColumn }) => { - const state = reactive({ - dropConfig, - collectColumn, - tableColumn, - colidMap: null - }) - - // 在设置 scheme 标志位 v2 时,列拖拽使用新方案 - if (!state.dropConfig || (state.dropConfig && state.dropConfig.scheme !== 'v2')) return +export const useDrag = ({ props, collectColumn, tableColumn }) => { + const state = hooks.reactive({ + dropConfig: hooks.toRef(props, 'dropConfig'), + collectColumn, + tableColumn, + colidMap: null + }) - const $table = getCurrentInstance().proxy + // 在设置 scheme 标志位 v2 时,列拖拽使用新方案 + if (!state.dropConfig || state.dropConfig?.scheme !== 'v2') return - // 列拖拽处理 - if (state.dropConfig.column) { - // 是否只允许同层级拖拽 - const isColumnGroupLevel = !state.dropConfig.columnGroup || state.dropConfig.columnGroup === 'level' - const stopHandlerMap = new Map() - const tableColumnWatch = createTableColumnWatch($table, state, isColumnGroupLevel, stopHandlerMap) + const $table = hooks.getCurrentInstance()?.proxy - watch(collectColumn, () => { - state.colidMap = getColidMap(state.collectColumn) - }) + // 列拖拽处理 + if (state.dropConfig?.column) { + // 是否只允许同层级拖拽 + const isColumnGroupLevel = !state.dropConfig?.columnGroup || state.dropConfig?.columnGroup === 'level' + const stopHandlerMap = new Map() + const tableColumnWatch = createTableColumnWatch($table, state, isColumnGroupLevel, stopHandlerMap) - watch(tableColumn, () => tableColumnWatch()) + hooks.watch(collectColumn, () => { + state.colidMap = getColidMap(state.collectColumn) + }) - onBeforeUnmount(() => { - if (stopHandlerMap.size > 0) { - const dndProxyList = [] + hooks.watch(tableColumn, () => tableColumnWatch()) - for (const [dndProxy, stopHander] of stopHandlerMap) { - dndProxyList.push(dndProxy) - stopHander.destroy() - } + hooks.onBeforeUnmount(() => { + if (stopHandlerMap.size > 0) { + const dndProxyList = [] - dndProxyList.forEach((dndProxy) => stopHandlerMap.delete(dndProxy)) + for (const [dndProxy, stopHander] of stopHandlerMap) { + dndProxyList.push(dndProxy) + stopHander.destroy() } - }) - } - } -export const useDrag = createUseDrag(hooks) + dndProxyList.forEach((dndProxy) => stopHandlerMap.delete(dndProxy)) + } + }) + } +} diff --git a/packages/vue/src/grid/src/composable/useHeader.ts b/packages/vue/src/grid/src/composable/useHeader.ts new file mode 100644 index 0000000000..9a0af49b21 --- /dev/null +++ b/packages/vue/src/grid/src/composable/useHeader.ts @@ -0,0 +1,116 @@ +import { hooks } from '@opentiny/vue-common' + +export const calcHeader = (collectColumn) => { + let maxLevel = 0 + const leafColumns = [] + const parentMap = new WeakMap() + const levelMap = new WeakMap() + + const traverseTree = (tree, level, parent) => { + if (Array.isArray(tree) && tree.length > 0) { + if (level > maxLevel) { + maxLevel = level + } + + tree.forEach((item) => { + if (parent) { + parentMap.set(item, parent) + } + + levelMap.set(item, level) + + traverseTree(item.children, level + 1, item) + }) + } else { + leafColumns.push(parent) + } + } + + traverseTree(collectColumn, 0, null) + + const headerTable = [] + const rowspanMap = new WeakMap() + + for (let i = 0; i <= maxLevel; i++) { + headerTable[i] = new Array(leafColumns.length).fill(0) + } + + leafColumns.forEach((column, index) => { + const level = levelMap.get(column) + + rowspanMap.set(column, maxLevel - level + 1) + headerTable[level][index] = column + + for (let l = level - 1; l >= 0; l--) { + column = headerTable[l][index] = parentMap.get(column) + } + }) + + return { leafColumns, headerTable, rowspanMap, maxLevel } +} + +const calcSpan = (tableColumn, header, headerRowHeight) => { + const indices = tableColumn.map((c) => header.leafColumns.indexOf(c)) + const subTable = [] + + header.headerTable.forEach((cols, i) => { + const countMap = new WeakMap() + + subTable[i] = indices + .map((j) => cols[j]) + .reduce((p, col) => { + if (col) { + if (!p.includes(col)) { + p.push(col) + } + + if (countMap.has(col)) { + countMap.set(col, countMap.get(col) + 1) + } else { + countMap.set(col, 1) + } + } + return p + }, []) + .map((column) => { + const rowspan = header.rowspanMap.get(column) || 1 + return { + id: column.id, + column, + colspan: countMap.get(column), + rowspan, + height: rowspan * headerRowHeight, + top: i * headerRowHeight + } + }) + }) + + return subTable +} + +export const useHeader = (props, bodyVm, headerRowHeight) => { + const headerTable = hooks.ref([]) + const $table = bodyVm.$parent + const { showHeader } = $table + const header = hooks.ref() + + hooks.watch( + () => props.collectColumn, + () => { + const head = (header.value = calcHeader(props.collectColumn)) + if (showHeader) { + $table.headerHeight = (head.maxLevel + 1) * headerRowHeight.value + } + } + ) + + hooks.watch([() => props.tableColumn, header], () => { + const head = header.value + + if (showHeader && head) { + headerTable.value = calcSpan(props.tableColumn, head, headerRowHeight.value) + } + }) + + return { headerTable } +} diff --git a/packages/vue/src/grid/src/composable/useRowGroup.ts b/packages/vue/src/grid/src/composable/useRowGroup.ts index 140db08dca..5fb141539e 100644 --- a/packages/vue/src/grid/src/composable/useRowGroup.ts +++ b/packages/vue/src/grid/src/composable/useRowGroup.ts @@ -1,61 +1,82 @@ import { hooks } from '@opentiny/vue-common' import { find } from '@opentiny/vue-renderless/grid/static' -const createUseRowGroup = - ({ reactive, watch, getCurrentInstance, onBeforeUnmount }) => - ({ rowGroup, visibleColumn, tableFullColumn, tableColumn }) => { - const state = reactive({ - rowGroup, - visibleColumn, - tableFullColumn, - tableColumn - }) +export const useRowGroup = ({ props, visibleColumn, tableFullColumn, tableColumn, columnStore }) => { + const state = hooks.reactive({ + rowGroup: hooks.toRef(props, 'rowGroup'), + visibleColumn, + tableFullColumn, + tableColumn, + columnStore + }) - if (!state.rowGroup) return + if (!state.rowGroup) return - const $table = getCurrentInstance().proxy + const $table = hooks.getCurrentInstance()?.proxy - watch([visibleColumn, tableColumn], () => { - // 取可见列中第一个rowGroup.field作为分组列 - let targetColumn = find(state.visibleColumn, (col) => col.property === state.rowGroup.field) + hooks.watch([visibleColumn, tableColumn], () => { + // 取可见列中第一个rowGroup.field作为分组列 + let targetColumn = find(state.visibleColumn, (col) => col.property === state.rowGroup.field) - // 如果rowGroup.field指定的列不存在或不可见,就取第一个field配置存在的列 - if (!targetColumn) { - targetColumn = find(state.visibleColumn, (col) => !!col.property) - } + // 如果rowGroup.field指定的列不存在或不可见,就取第一个field配置存在的列 + if (!targetColumn) { + targetColumn = find(state.visibleColumn, (col) => !!col.property) + } + + if (targetColumn) { + $table._rowGroupTargetColumn = targetColumn + + const index = state.tableColumn.indexOf(targetColumn) - if (targetColumn) { - $table._rowGroupTargetColumn = targetColumn + const length = state.tableColumn.length + let targetColumnColspan = state.rowGroup.colspan || 1 - const index = state.tableColumn.indexOf(targetColumn) - const length = state.tableColumn.length - let targetColumnColspan = state.rowGroup.colspan || 1 + targetColumnColspan = Math.max(targetColumnColspan, 1) - targetColumnColspan = Math.max(targetColumnColspan, 1) + if (targetColumnColspan > 1) { + let leftIndex = -1 + let rightIndex = -1 - if (targetColumnColspan > 1) { - targetColumnColspan = Math.min(targetColumnColspan, length - index) + if ((leftIndex = state.columnStore.leftList.length) > 0) { + leftIndex = leftIndex - 1 } - for (let i = 0; i < length; i++) { - const vCol = state.tableColumn[i] + if (state.columnStore.rightList.length > 0) { + rightIndex = state.tableColumn.indexOf(state.columnStore.rightList[0]) - 1 + } + + let max - if (vCol === targetColumn) { - vCol._rowGroupColspan = targetColumnColspan - } else { - vCol._rowGroupColspan = i > index && i < index + targetColumnColspan ? 0 : 1 - } + if (leftIndex > -1 && index <= leftIndex) { + max = leftIndex - index + 1 + } else if (rightIndex > -1 && index <= rightIndex) { + max = rightIndex - index + 1 + } else { + max = length - index } + + targetColumnColspan = Math.min(targetColumnColspan, max) } - }) - onBeforeUnmount(() => { - delete $table._rowGroupTargetColumn + for (let i = 0; i < length; i++) { + const vCol = state.tableColumn[i] - state.tableFullColumn.forEach((column) => { - delete column._rowGroupColspan - }) - }) - } + if (vCol === targetColumn) { + vCol._rowGroupColspan = targetColumnColspan + vCol._stickyClass = 'fixed-left-last__column' + } else { + vCol._rowGroupColspan = i > index && i < index + targetColumnColspan ? 0 : 1 + } + } + } + }) + + hooks.onBeforeUnmount(() => { + delete $table._rowGroupTargetColumn -export const useRowGroup = createUseRowGroup(hooks) + state.tableFullColumn.forEach((column) => { + delete column._rowGroupColspan + delete column._stickyClass + }) + }) +} diff --git a/packages/vue/src/grid/src/config.ts b/packages/vue/src/grid/src/config.ts index 8885059514..148dae54f5 100644 --- a/packages/vue/src/grid/src/config.ts +++ b/packages/vue/src/grid/src/config.ts @@ -41,6 +41,7 @@ const GlobalConfig = { message: 'tooltip', icon: iconError() }, + editConfig: { trigger: 'click', mode: 'cell', showStatus: true }, // 默认开启点击头部单元格触发排序 sortConfig: { multipleColumnSort: false }, // 默认不开启隔行换色和行高亮,不暴露此配置 @@ -65,6 +66,7 @@ const GlobalConfig = { optimization: { animat: true, delayHover: 250, + scrollDelay: 60, scrollX: { gt: 100 }, @@ -166,6 +168,16 @@ const GlobalConfig = { TINY: 'tiny', SAAS: 'saas' }, + rowHeight: { + tiny: { mini: 32, small: 40, default: 48, medium: 52 }, + saas: { mini: 32, small: 36, default: 36, medium: 40 } + }, + headerRowHeight: { + tiny: { mini: 28, small: 32, default: 40, medium: 40 }, + saas: { mini: 32, small: 36, default: 36, medium: 40 } + }, + // 空数据最小表格高度 + emptyMinHeight: 200, columnLevelKey: 'ColumnLevelProvideKey', defaultColumnName: $prefix + 'GridColumn' } diff --git a/packages/vue/src/grid/src/dragger/src/methods.ts b/packages/vue/src/grid/src/dragger/src/methods.ts index 04ee3d874f..92d703a055 100644 --- a/packages/vue/src/grid/src/dragger/src/methods.ts +++ b/packages/vue/src/grid/src/dragger/src/methods.ts @@ -5,7 +5,7 @@ export default { // 处理列拖拽 columnDrop(headerEl) { const { plugin, onBeforeMove, filter } = this.dropConfig || {} - const columnDropContainer = headerEl.querySelector('.tiny-grid__header .tiny-grid-header__row') + const columnDropContainer = headerEl.querySelector('.tiny-grid-header__row') const columnDropOptions = { ...this.dropConfig, diff --git a/packages/vue/src/grid/src/edit/src/methods.ts b/packages/vue/src/grid/src/edit/src/methods.ts index b89d59fd63..936703f28d 100644 --- a/packages/vue/src/grid/src/edit/src/methods.ts +++ b/packages/vue/src/grid/src/edit/src/methods.ts @@ -156,7 +156,7 @@ export default { operArrs({ _vm: this, editStore, newRecords, newRecordsCopy, nowData, row, tableFullData, tableSourceData }) - this.updateCache(true) + this.updateCache() this.handleTableData(true) this.checkSelectionStatus() this.updateFooter() @@ -234,7 +234,7 @@ export default { remove(insertList, (row) => inArr(row, rows)) // 修改缓存 - this.updateCache(true) + this.updateCache() this.handleTableData(true) this.checkSelectionStatus() @@ -300,6 +300,8 @@ export default { destructuring(row, oRow) } } + + this.updateRowStatus(row) } if (arguments.length) { @@ -420,6 +422,7 @@ export default { } if (isActived) { + this.updateRowStatus(actived.row) this.updateFooter() // 处理数字输入框返回string类型数据,导致还原初始数字还是编辑状态的问题 @@ -558,7 +561,7 @@ export default { /** * 处理选中源 */ - handleSelected(params, event) { + handleSelected(params, event, noDebounce) { let { editConfig, editStore, elemStore, mouseConfig = {} } = this let { actived, selected } = editStore let { cell, column, row } = params || {} @@ -589,7 +592,7 @@ export default { return this.$nextTick() } // 如果配置了批量选中功能,则为批量选中状态 - let headerElem = elemStore['main-header-list'] + let headerElem = elemStore['main-body-headerList'] this.handleChecked([[cell]]) @@ -597,13 +600,13 @@ export default { return this.$nextTick() } - this.handleHeaderChecked([[headerElem.querySelector(`.${column && column.id}`)]]) - this.handleIndexChecked([[cell && cell.parentNode && cell.parentNode.querySelector('.col__index')]]) + this.handleHeaderChecked([[headerElem?.querySelector(`.${column?.id}`)]]) + this.handleIndexChecked([[cell?.parentNode?.querySelector('.col__index')]]) return this.$nextTick() } - selectMethod = debounce(20, selectMethod) + selectMethod = noDebounce ? selectMethod : debounce(20, selectMethod) return selectMethod() } diff --git a/packages/vue/src/grid/src/fetch-data/src/methods.ts b/packages/vue/src/grid/src/fetch-data/src/methods.ts index db9c8a1f6a..4084c50573 100644 --- a/packages/vue/src/grid/src/fetch-data/src/methods.ts +++ b/packages/vue/src/grid/src/fetch-data/src/methods.ts @@ -25,6 +25,8 @@ export default { }, handleFetch(code, sortArg) { let { pager, sortData, filterData, pagerConfig, fetchOption, fetchData, dataset } = this as any + let { reloadConfig = {} } = fetchData + let { scroll = false } = reloadConfig if (this.isInitialLoading) { this.isInitialLoading = false @@ -34,7 +36,7 @@ export default { if (code !== 'prefetch') { this.clearRadioRow() - this.resetScrollTop() + !scroll && this.resetScrollTop() } if (!fetchOption) { diff --git a/packages/vue/src/grid/src/footer/index.ts b/packages/vue/src/grid/src/footer/index.ts deleted file mode 100644 index 4f71716b40..0000000000 --- a/packages/vue/src/grid/src/footer/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2019 Xu Liangzhan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import Footer from './src/footer' - -Footer.install = function (Vue) { - Vue.component(Footer.name, Footer) -} - -export default Footer diff --git a/packages/vue/src/grid/src/footer/src/footer.ts b/packages/vue/src/grid/src/footer/src/footer.ts deleted file mode 100644 index 6d462f6555..0000000000 --- a/packages/vue/src/grid/src/footer/src/footer.ts +++ /dev/null @@ -1,359 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2019 Xu Liangzhan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { isFunction } from '@opentiny/vue-renderless/grid/static/' -import { getClass, emitEvent, formatText, updateCellTitle } from '@opentiny/vue-renderless/grid/utils' -import { isNull } from '@opentiny/utils' -import { h, $prefix, defineComponent } from '@opentiny/vue-common' - -const classMap = { - fixedHidden: 'fixed__column', - colEllipsis: 'col__ellipsis', - filterActive: 'filter__active', - cellSummary: 'cell__summary', - fixedLeftLast: 'fixed-left-last__column', - fixedRightFirst: 'fixed-right-first__column', - colRadio: 'col__radio', - colSelection: 'col__selection' -} - -function doFooterSpan({ attrs, footerData, footerSpanMethod, params }) { - if (footerSpanMethod) { - let { rowspan = 1, colspan = 1 } = footerSpanMethod({ data: footerData, ...params }) || {} - - if (!rowspan || !colspan) { - return null - } - - attrs.rowspan = rowspan - attrs.colspan = colspan - } -} - -function addListenerDblclick({ $table, params, tableListeners, tfOns }) { - if (tableListeners['footer-cell-dblclick']) { - tfOns.dblclick = (event) => { - emitEvent($table, 'footer-cell-dblclick', [{ cell: event.currentTarget, ...params }, event]) - } - } -} - -function addListenerClick({ $table, params, tableListeners, tfOns }) { - if (tableListeners['footer-cell-click']) { - tfOns.click = (event) => { - emitEvent($table, 'footer-cell-click', [{ cell: event.currentTarget, ...params }, event]) - } - } -} - -function addListenerMouseout({ $table, showTooltip, tfOns }) { - if (showTooltip) { - tfOns.mouseout = () => { - $table.clostTooltip() - } - } -} - -function addListenerMouseover({ $table, params, showTitle, showTooltip, tfOns }) { - if (showTitle || showTooltip) { - tfOns.mouseover = (event) => { - if (showTitle) { - updateCellTitle(event) - } else if (showTooltip) { - $table.triggerFooterTooltipEvent(event, params) - } - } - } -} - -function renderColgroup(tableColumn) { - return h( - 'colgroup', - { ref: 'colgroup' }, - tableColumn - .map((column, columnIndex) => h('col', { attrs: { name: column.id }, key: columnIndex })) - .concat([h('col', { attrs: { name: 'col_gutter' } })]) - ) -} - -const renderfoots = (opt) => { - const { $table, allAlign, allColumnOverflow, allFooterAlign, buildParamFunc, columnKey, columnStore } = opt - const { - footerCellClassName, - footerData, - footerRowClassName, - footerSpanMethod, - overflowX, - tableColumn, - tableListeners - } = opt - const { scrollbarWidth } = $table - return (list, $rowIndex) => - h( - 'tr', - { - class: [ - 'tiny-grid-footer__row', - footerRowClassName - ? isFunction(footerRowClassName) - ? footerRowClassName({ $table, $rowIndex }) - : footerRowClassName - : '' - ] - }, - tableColumn - .map((column, $columnIndex) => { - const arg1 = { $columnIndex, $rowIndex, $table, allAlign, allColumnOverflow, allFooterAlign } - const arg2 = { column, footerData, footerSpanMethod, overflowX, tableListeners } - const { - attrs, - columnIndex, - fixedHiddenColumn, - footAlign, - footerClassName, - hasEllipsis, - params, - tfOns, - isShowEllipsis, - isShowTitle, - showTooltip - } = buildParamFunc(Object.assign(arg1, arg2)) - const { leftList, rightList } = columnStore - const { left: leftPosition, right } = column.style || {} - // 表尾右侧冻结列,当有表体有滚动条时,需要加上滚动条的偏移量 - const rightPosition = right >= 0 ? right + scrollbarWidth : '' - return h( - 'td', - { - class: [ - 'tiny-grid-footer__column', - column.id, - { - [`col__${footAlign}`]: footAlign, - [classMap.fixedHidden]: fixedHiddenColumn, - [classMap.colEllipsis]: hasEllipsis, - [classMap.filterActive]: column.filter && column.filter.hasFilter, - [classMap.fixedLeftLast]: column.fixed === 'left' && leftList[leftList.length - 1] === column, - [classMap.fixedRightFirst]: column.fixed === 'right' && rightList[0] === column, - [classMap.colRadio]: column.type === 'radio', - [classMap.colSelection]: column.type === 'selection' - }, - getClass(footerClassName, params), - getClass(footerCellClassName, params) - ], - style: fixedHiddenColumn - ? { - left: `${leftPosition}px`, - right: `${rightPosition}px` - } - : null, - attrs, - on: tfOns, - key: columnKey ? column.id : columnIndex - }, - [ - h( - 'div', - { - class: [ - 'tiny-grid-cell', - { - [classMap.cellSummary]: $table.summaryConfig, - 'tiny-grid-cell__title': isShowTitle, - 'tiny-grid-cell__tooltip': showTooltip || column.showTip, - 'tiny-grid-cell__ellipsis': isShowEllipsis - } - ] - }, - // 如果不是表格形态,就只保留表格结构(到tiny-grid-cell),不渲染具体的内容 - $table.isShapeTable ? formatText(list[$table.tableColumn.indexOf(column)], 1) : null - ) - ] - ) - }) - .concat([h('td', { class: 'col__gutter' })]) - ) -} - -function renderTfoot(opt) { - return h('tfoot', { ref: 'tfoot' }, opt.footerData.map(renderfoots(opt))) -} - -export default defineComponent({ - name: `${$prefix}GridFooter`, - props: { - fixedColumn: Array, - fixedType: String, - footerData: Array, - size: String, - tableColumn: Array, - visibleColumn: Array - }, - mounted() { - let { $el, $parent: $table, $refs } = this - let { elemStore } = $table - let keyPrefix = 'main-footer-' - - elemStore[`${keyPrefix}wrapper`] = $el - elemStore[`${keyPrefix}table`] = $refs.table - elemStore[`${keyPrefix}colgroup`] = $refs.colgroup - elemStore[`${keyPrefix}list`] = $refs.tfoot - elemStore[`${keyPrefix}x-space`] = $refs.xSpace - }, - render() { - let { $parent: $table, buildParamFunc, fixedColumn, fixedType, footerData, tableColumn } = this - let { - align: allAlign, - columnKey, - footerAlign: allFooterAlign, - footerCellClassName, - footerRowClassName, - footerSpanMethod, - columnStore - } = $table - let { overflowX, showOverflow: allColumnOverflow, tableLayout, tableListeners, renderFooter } = $table - - let tableAttrs = { cellspacing: 0, cellpadding: 0, border: 0 } - let colgroupVNode = renderColgroup(tableColumn) - let arg1 = { $table, allAlign, allColumnOverflow, allFooterAlign, buildParamFunc, columnKey, columnStore } - let arg2 = { - footerCellClassName, - footerData, - footerRowClassName, - footerSpanMethod, - overflowX, - tableColumn, - tableListeners - } - let tfootVNode = renderTfoot(Object.assign(arg1, arg2)) - - const renderParams = { $table, columns: tableColumn, footerData, fixedColumns: fixedColumn, fixedType } - - return h( - 'div', - { - class: ['tiny-grid__footer-wrapper', 'body__wrapper'], - on: { scroll: this.scrollEvent } - }, - [ - h('div', { class: 'tiny-grid-body__x-space', ref: 'xSpace' }), - typeof renderFooter === 'function' - ? renderFooter(renderParams, h) - : h( - 'table', - { - class: 'tiny-grid__footer', - style: { tableLayout }, - attrs: tableAttrs, - ref: 'table' - }, - [ - // 列宽 - colgroupVNode, - // 底部 - tfootVNode - ] - ) - ] - ) - }, - methods: { - scrollEvent(event) { - // 滚动处理: 如果存在列固定左侧,同步更新滚动状态;如果存在列固定右侧,同步更新滚动状态。 - let { $parent: $table } = this - let { $refs, lastScrollLeft, scrollXLoad } = $table - let { tableBody, tableFooter, tableHeader } = $refs - let headerElem = tableHeader ? tableHeader.$el : null - let bodyElem = tableBody ? tableBody.$el : null - let footerElem = tableFooter ? tableFooter.$el : null - let scrollLeft = footerElem.scrollLeft - let isX = scrollLeft !== lastScrollLeft - let setElemScrollLeft = (elem, scrollLeft) => { - if (elem) { - elem.scrollLeft = scrollLeft - } - } - let eventParams = [{ $table, isX, isY: false, scrollLeft, scrollTop: bodyElem.scrollTop, type: 'footer' }, event] - - $table.lastScrollTime = Date.now() - $table.lastScrollLeft = scrollLeft - - setElemScrollLeft(headerElem, scrollLeft) - setElemScrollLeft(bodyElem, scrollLeft) - - if (scrollXLoad && isX) { - $table.triggerScrollXEvent(event) - } - - emitEvent($table, 'scroll', eventParams) - }, - buildParamFunc(opt) { - let { $columnIndex, $rowIndex, $table, allAlign, allColumnOverflow, allFooterAlign } = opt - let { column, footerData, footerSpanMethod, tableListeners } = opt - let { showOverflow, footerAlign, align, footerClassName } = column - let fixedHiddenColumn = column.fixed - let cellOverflowValue = isNull(showOverflow) ? allColumnOverflow : showOverflow - let footAlign = footerAlign || align || allFooterAlign || allAlign - let isShowEllipsis = cellOverflowValue === 'ellipsis' - let isShowTitle = cellOverflowValue === 'title' - let showTooltip = cellOverflowValue === true || cellOverflowValue === 'tooltip' - let hasEllipsis = isShowTitle || showTooltip || isShowEllipsis - let attrs = { 'data-colid': column.id } - let tfOns = {} - let columnIndex = $table.getColumnIndex(column) - let params = { - $table, - $rowIndex, - column, - columnIndex, - $columnIndex - } - - addListenerMouseover({ $table, params, showTitle: isShowTitle, showTooltip, tfOns }) - - addListenerMouseout({ $table, showTooltip, tfOns }) - - addListenerClick({ $table, params, tableListeners, tfOns }) - - addListenerDblclick({ $table, params, tableListeners, tfOns }) - // 处理行或者列的合并 - doFooterSpan({ attrs, footerData, footerSpanMethod, params }) - - return { - attrs, - columnIndex, - fixedHiddenColumn, - footAlign, - footerClassName, - hasEllipsis, - isShowEllipsis, - isShowTitle, - showTooltip, - params, - tfOns - } - } - } -}) diff --git a/packages/vue/src/grid/src/grid/grid.ts b/packages/vue/src/grid/src/grid/grid.ts index f6047336c4..aa94cda7b2 100644 --- a/packages/vue/src/grid/src/grid/grid.ts +++ b/packages/vue/src/grid/src/grid/grid.ts @@ -81,7 +81,7 @@ function createRender(opt) { } }, [ - selectToolbar ? null : renderedToolbar, + selectToolbar ? null : renderedToolbar(), columnAnchor ? _vm.renderColumnAnchor(columnAnchorParams, _vm) : null, // 这里会渲染tiny-grid-column插槽内容,从而获取列配置 h(TinyGridTable, { props, on: tableOns, ref: 'tinyTable' }, slots.default && slots.default()), @@ -159,7 +159,8 @@ export default defineComponent({ columnAnchorKey: '', tasks: {}, fullScreenClass: '', - isInitialLoading: true // 是否首次加载数据 + isInitialLoading: true, // 是否首次加载数据 + _delayActivateAnchor: undefined } }, computed: { @@ -201,6 +202,19 @@ export default defineComponent({ }, isViewCustom() { return this.viewType === V_CUSTOM + }, + optimizOpt() { + return Object.assign(this.initOptimization, GlobalConfig.optimization, this.optimization) + }, + editConfigOpt() { + return this.editConfig + ? Object.assign(this.initEditConfig, GlobalConfig.editConfig, this.editConfig, { + activeMethod: this.handleActiveMethod + }) + : null + }, + tooltipOpt() { + return Object.assign(this.initTooltipConfig, GlobalConfig.tooltip, this.designConfig?.tooltip, this.tooltipConfig) } }, watch: { @@ -211,13 +225,10 @@ export default defineComponent({ tableCustoms() { this.toolbar && this.$refs.toolbar && this.$refs.toolbar.loadStorage() }, - columnAnchorParams() { - setTimeout(() => this.emitter.emit('active-anchor'), this.columnAnchorParams.activeAnchor.delay) - }, viewType(value) { // 解决从卡片、列表视图切换至表格视图后,列宽未自动撑开问题 if (value === V_MF) { - this.$nextTick(() => this.recalculate(true)) + this.$nextTick(() => this.recalculate()) } } }, @@ -305,11 +316,12 @@ export default defineComponent({ if (this.isMultipleHistory) { this.initMultipleHistory() } - - this.addIntersectionObserver() }, setup(props, context) { const { listeners, attrs } = context + const initEditConfig = hooks.ref({}) + const initOptimization = hooks.ref({}) + const initTooltipConfig = hooks.ref({}) // 处理表格用户传递过来的事件监听 const tableListeners = getListeners(attrs, listeners) const tinyTheme = hooks.ref(resolveTheme(props, context)) @@ -317,13 +329,20 @@ export default defineComponent({ const breakpoint = useBreakpoint() const renderless = (props, hooks, { designConfig = null }) => { - return { tableListeners, designConfig, tinyTheme, tinyMode, currentBreakpoint: breakpoint.current } + return { + tableListeners, + designConfig, + tinyTheme, + tinyMode, + currentBreakpoint: breakpoint.current, + initEditConfig, + initOptimization, + initTooltipConfig + } } hooks.onBeforeUnmount(() => { const gridVm = hooks.getCurrentInstance().proxy - - gridVm.removeIntersectionObserver() // 清空被缓存实例 gridVm.vmStore = null }) @@ -332,16 +351,26 @@ export default defineComponent({ props, context, renderless, - api: ['designConfig', 'tableListeners', 'tinyTheme', 'tinyMode', 'currentBreakpoint'] + api: [ + 'designConfig', + 'tableListeners', + 'tinyTheme', + 'tinyMode', + 'currentBreakpoint', + 'initEditConfig', + 'initOptimization', + 'initTooltipConfig' + ] }) }, render() { const { - editConfig, fetchOption, listeners, loading, - optimization, + optimizOpt, + editConfigOpt, + tooltipOpt, pager, pagerConfig, remoteFilter, @@ -368,17 +397,12 @@ export default defineComponent({ Object.assign(GlobalConfig.icon, designConfig.icons) } - // 初始化虚拟滚动优化配置 - const optimizOpt = { ...GlobalConfig.optimization, ...optimization } - const props = { ...tableProps, optimization: optimizOpt, startIndex: seqIndex } - - // 初始化 tooltip 配置 - props.tooltipConfig = Object.assign( - {}, - GlobalConfig.tooltip || {}, - designConfig?.tooltip || {}, - props.tooltipConfig || {} - ) + const props = Object.assign(tableProps, { + optimization: optimizOpt, + startIndex: seqIndex, + editConfig: editConfigOpt, + tooltipConfig: tooltipOpt + }) // 在用户没有配置stripe时读取design配置 if (designConfig?.stripe !== undefined && !props.stripe) { @@ -386,7 +410,7 @@ export default defineComponent({ props.stripe = designConfig?.stripe } - const tableOns = { ...listeners, ...tableListeners } + const tableOns = Object.assign(listeners, tableListeners) const { handleRowClassName: rowClassName, sortChangeEvent, filterChangeEvent } = this // fetchApi状态下初始化 loading、remoteSort、remoteFilter @@ -402,19 +426,8 @@ export default defineComponent({ // 列就绪事件处理 tableOns['column-init-ready'] = this.handleColumnInitReady - // 这里handleActiveMethod处理一些编辑器的声明周期的拦截,用户传递过来的activeMethod优先级最高 - if (editConfig) { - props.editConfig = { - trigger: 'click', - mode: 'cell', - showStatus: true, - ...editConfig, - activeMethod: this.handleActiveMethod - } - } - // 获取工具栏的渲染器 - const renderedToolbar = this.getRenderedToolbar({ $slots, _vm: this, loading, tableLoading, toolbar }) + const renderedToolbar = () => this.getRenderedToolbar({ $slots, _vm: this, loading, tableLoading, toolbar }) // 创建表格最外层容器,并加载table组件 return createRender({ @@ -527,34 +540,6 @@ export default defineComponent({ viewCls(module) { return GlobalConfig.viewConfig[module][this.viewType] || '' }, - // 监听某个元素是否出现在视口中 - addIntersectionObserver() { - if ((this.intersectionOption && this.intersectionOption.disabled) || typeof IntersectionObserver === 'undefined') - return - - this.intersectionObserver = new IntersectionObserver((entries) => { - let entry = entries[0] - - if (entries.length > 1) { - const intersectingEntry = entries.find((entry) => entry.isIntersecting) - - if (intersectingEntry) { - entry = intersectingEntry - } - } - - this.handleVisibilityChange(entry.isIntersecting, entry) - }, this.intersectionOption) - - this.intersectionObserver.observe(this.$el) - }, - removeIntersectionObserver() { - if (this.intersectionObserver) { - this.intersectionObserver.unobserve(this.$el) - this.intersectionObserver.disconnect() - this.intersectionObserver = null - } - }, filterChangeEvent(params) { let eventParams = extend(false, { $grid: this }, params) // 如果是服务端过滤 diff --git a/packages/vue/src/grid/src/header/index.ts b/packages/vue/src/grid/src/header/index.ts deleted file mode 100644 index b58f841032..0000000000 --- a/packages/vue/src/grid/src/header/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2019 Xu Liangzhan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import Header from './src/header' - -Header.install = function (Vue) { - Vue.component(Header.name, Header) -} - -export default Header diff --git a/packages/vue/src/grid/src/header/src/header.ts b/packages/vue/src/grid/src/header/src/header.ts deleted file mode 100644 index 5f4b8679f8..0000000000 --- a/packages/vue/src/grid/src/header/src/header.ts +++ /dev/null @@ -1,579 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2019 Xu Liangzhan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -import { isObject, isNull } from '@opentiny/utils' -import { removeClass, addClass } from '@opentiny/utils' -import { isBoolean, isFunction } from '@opentiny/vue-renderless/grid/static/' -import { updateCellTitle, emitEvent, getClass } from '@opentiny/vue-renderless/grid/utils' -import { h, $prefix, defineComponent } from '@opentiny/vue-common' -import { random } from '@opentiny/utils' - -function addListenerMousedown({ $table, mouseConfig, params, thOns }) { - if (mouseConfig.checked) { - thOns.mousedown = (event) => - $table.triggerHeaderCellMousedownEvent(event, { - cell: event.currentTarget, - ...params - }) - } -} - -function addListenerDblclick({ $table, params, tableListeners, thOns }) { - if (tableListeners['header-cell-dblclick']) { - thOns.dblclick = (event) => - emitEvent($table, 'header-cell-dblclick', [{ cell: event.currentTarget, ...params }, event]) - } -} - -function addListenerClick({ $table, highlightCurrentColumn, mouseConfig, params, sortOpts, tableListeners, thOns }) { - if ( - highlightCurrentColumn || - tableListeners['header-cell-click'] || - mouseConfig.checked || - sortOpts.trigger === 'cell' - ) { - thOns.click = (event) => - $table.triggerHeaderCellClickEvent(event, { - cell: event.currentTarget, - ...params - }) - } -} - -function addListenerMouseout({ $table, showHeaderTip, showTooltip, thOns }) { - if (showTooltip || showHeaderTip) { - thOns.mouseout = () => { - if ($table._isResize) { - return - } - - $table.clostTooltip() - } - } -} - -function addListenerMouseover({ $table, params, showHeaderTip, showTitle, showTooltip, thOns }) { - if (showTitle || showTooltip || showHeaderTip) { - thOns.mouseover = (event) => { - if ($table._isResize) { - return - } - - if (showTitle) { - updateCellTitle(event) - } else if (showTooltip || showHeaderTip) { - $table.triggerHeaderTooltipEvent(event, { showHeaderTip, ...params }) - } - } - } -} - -function modifyHeadAlign({ column, headAlign }) { - if (~['radio', 'selection', 'index'].indexOf(column.type)) { - headAlign = headAlign || 'center' - } - - return headAlign -} - -function computeDragLeft(args) { - let { dragMinLeft, resizableConfig, scrollLeft, column, startColumnLeft, left } = args - - let dragLeft = Math.max(left, dragMinLeft) - - if (resizableConfig?.limit instanceof Function) { - let currentMouseLeft = dragLeft - scrollLeft - let width = resizableConfig.limit({ field: column.own.field, width: currentMouseLeft - startColumnLeft }) - dragLeft = startColumnLeft + width - } - - return { left, dragMinLeft, dragLeft } -} - -function renderTableColgroup(tableColumn) { - return h( - 'colgroup', - { - ref: 'colgroup' - }, - tableColumn - .map((column, columnIndex) => h('col', { attrs: { name: column.id }, key: columnIndex })) - .concat([h('col', { attrs: { name: 'col_gutter' } })]) - ) -} - -function renderRepair() { - return h('div', { class: 'tiny-grid__repair', ref: 'repair' }) -} - -function renderXSpace() { - return h('div', { class: 'tiny-grid-body__x-space', ref: 'xSpace' }) -} - -const classMap = { - colFixed: 'col__fixed', - colIndex: 'col__index', - colRadio: 'col__radio', - colSelection: 'col__selection', - colGroup: 'col__group', - colEllipsis: 'col__ellipsis', - fixedHidden: 'fixed__column', - isSortable: 'is__sortable', - isEditable: 'is__editable', - isFilter: 'is__filter', - filterActive: 'filter__active' -} - -function getThPropsArg(args) { - let { column, columnIndex, columnKey, fixedHiddenColumn, hasEllipsis, headAlign, columnStore } = args - let { headerCellClassName, headerClassName, isColGroup, isDragHeaderSorting, params, thOns, scrollbarWidth } = args - const { leftList, rightList } = columnStore - - return { - class: [ - 'tiny-grid-header__column', - column.id, - { - [`col__${headAlign}`]: headAlign, - [classMap.colFixed]: column.fixed, - [classMap.colIndex]: column.type === 'index', - [classMap.colRadio]: column.type === 'radio', - [classMap.colSelection]: column.type === 'selection', - [classMap.colGroup]: isColGroup, - [classMap.colEllipsis]: hasEllipsis, - [classMap.fixedHidden]: fixedHiddenColumn, - [classMap.isSortable]: !['index', 'radio', 'selection'].includes(column.type) && column.sortable, - [classMap.isEditable]: column.editor, - [classMap.isFilter]: isObject(column.filter), - [classMap.filterActive]: column.filter && column.filter.hasFilter, - 'fixed-left-last__column': - column.fixed === 'left' && (leftList[leftList.length - 1] === column || column.isFixedLeftLast), - 'fixed-right-first__column': column.fixed === 'right' && (rightList[0] === column || column.isFixedRightFirst) - }, - getClass(headerClassName, params), - getClass(headerCellClassName, params) - ], - attrs: { - 'data-colid': column.id, - colspan: column.colSpan, - rowspan: column.rowSpan - }, - style: fixedHiddenColumn - ? { - left: `${column.style?.left}px`, - right: `${column.style?.right + scrollbarWidth}px` - } - : null, - on: thOns, - key: isDragHeaderSorting ? random() : columnKey || isColGroup ? column.id : columnIndex - } -} - -function renderThPartition({ border, column, isColGroup, resizable }) { - let res = null - - const classMap = { - isLine: 'is__line' - } - - if (!isColGroup && !(isBoolean(column.resizable) ? column.resizable : resizable) && column.type !== 'index') { - res = h('div', { - class: ['tiny-grid-thead-partition', { [classMap.isLine]: !border }] - }) - } - - return res -} - -function renderThCell(args) { - let { column, fixedHiddenColumn, headerSuffixIconAbsolute, params, $table } = args - let { showEllipsis, showHeaderTip, showTitle, showTooltip } = args - - return h( - 'div', - { - class: [ - 'tiny-grid-cell', - { - 'tiny-grid-cell__title': showTitle, - 'tiny-grid-cell__tooltip': showTooltip || showHeaderTip, - 'tiny-grid-cell__ellipsis': showEllipsis, - 'tiny-grid-cell__header-suffix': headerSuffixIconAbsolute - } - ] - }, - // 如果不是表格形态,就只保留表格结构(到tiny-grid-cell),不渲染具体的内容 - $table.isShapeTable ? column.renderHeader(h, { isHidden: fixedHiddenColumn, ...params }) : null - ) -} -function renderThResize({ _vm, border, column, fixedHiddenColumn, isColGroup, params, resizable, isColResize }) { - let res = null - - const classMap = { - isLine: 'is__line' - } - - // 删除fixedHiddenColumn,冻结表头放开可以拖拽调节宽度。 - if (!isColGroup && isColResize && (isBoolean(column.resizable) ? column.resizable : resizable)) { - res = h('div', { - class: ['tiny-grid-resizable', { [classMap.isLine]: !border }], - on: { - mousedown: (event) => _vm.resizeMousedown(event, { isHidden: fixedHiddenColumn, ...params }) - } - }) - } - - return res -} - -function getThHandler(args) { - let { - $rowIndex, - $table, - _vm, - allAlign, - allColumnHeaderOverflow, - allHeaderAlign, - border, - columnKey, - headerCellClassName - } = args - let { - headerSuffixIconAbsolute, - highlightCurrentColumn, - isDragHeaderSorting, - mouseConfig, - resizable, - sortOpts, - tableListeners - } = args - - let { operationColumnResizable } = $table - - return (column, $columnIndex) => { - let { showHeaderOverflow, showHeaderTip, headerAlign, align, headerClassName } = column - let isColGroup = column.children && column.children.length - let fixedHiddenColumn = column.fixed - let headOverflow = isNull(showHeaderOverflow) ? allColumnHeaderOverflow : showHeaderOverflow - let showEllipsis = headOverflow === 'ellipsis' - let showTitle = headOverflow === 'title' - let headAlign = headerAlign || align || allHeaderAlign || allAlign - let showTooltip = headOverflow === true || headOverflow === 'tooltip' - let thOns = {} - let hasEllipsis = showTitle || showTooltip || showEllipsis - const { columnStore, scrollbarWidth } = $table - - // type为index或radio或selection的列使用operationColumnResizable控制是否可拖动列宽,其它列默认是true - let isColResize = ['index', 'radio', 'selection'].includes(column.type) ? operationColumnResizable : true - - // 索引列、选择列如果不配置对齐方式则默认为居中对齐 - headAlign = modifyHeadAlign({ column, headAlign }) - // 确保表格索引的准确性 - let columnIndex = $table.getColumnIndex(column) - let params = { $table, $rowIndex, column } - Object.assign(params, { columnIndex, $columnIndex }) - addListenerMouseover({ $table, params, showHeaderTip, showTitle, showTooltip, thOns }) - addListenerMouseout({ $table, showHeaderTip, showTooltip, thOns }) - - let args1 = { $table, highlightCurrentColumn, mouseConfig, params } - Object.assign(args1, { sortOpts, tableListeners, thOns }) - addListenerClick(args1) - addListenerDblclick({ $table, params, tableListeners, thOns }) - - // 按下事件处理 - addListenerMousedown({ $table, mouseConfig, params, thOns }) - args1 = { column, columnIndex, columnKey, fixedHiddenColumn, hasEllipsis, headAlign, columnStore, scrollbarWidth } - Object.assign(args1, { headerCellClassName, headerClassName, isColGroup, isDragHeaderSorting, params, thOns }) - let args2 = { column, fixedHiddenColumn, headerSuffixIconAbsolute, params, $table } - Object.assign(args2, { showEllipsis, showHeaderTip, showTitle, showTooltip }) - - return h('th', getThPropsArg(args1), [ - renderThPartition({ border, column, isColGroup, resizable }), - renderThCell(args2), - // 列宽拖动 - renderThResize({ _vm, border, column, fixedHiddenColumn, isColGroup, params, resizable, isColResize }) - ]) - } -} - -function renderTableThead(args) { - let { $table, _vm, allAlign, allColumnHeaderOverflow } = args - let { allHeaderAlign, border, columnKey } = args - let { headerCellClassName, headerColumn, headerRowClassName, headerSuffixIconAbsolute } = args - let { highlightCurrentColumn, isDragHeaderSorting, mouseConfig } = args - let { overflowX, resizable, sortOpts, tableListeners } = args - - return h( - 'thead', - { - ref: 'thead' - }, - headerColumn.map((cols, $rowIndex) => { - let args1 = { $rowIndex, $table, _vm, allAlign, allColumnHeaderOverflow, allHeaderAlign, border, columnKey } - - Object.assign(args1, { headerCellClassName, headerSuffixIconAbsolute, highlightCurrentColumn }) - Object.assign(args1, { isDragHeaderSorting, mouseConfig, overflowX, resizable, sortOpts, tableListeners }) - - return h( - 'tr', - { - class: [ - 'tiny-grid-header__row', - headerRowClassName - ? isFunction(headerRowClassName) - ? headerRowClassName({ $table, $rowIndex }) - : headerRowClassName - : '' - ] - }, - cols.map(getThHandler(args1)).concat([h('th', { class: 'col__gutter' })]) - ) - }) - ) -} - -function updateResizableToolbar($table) { - const toolbarVm = $table.getVm('toolbar') - - if (toolbarVm) { - toolbarVm.updateResizable() - } -} - -function renderTable(args) { - let { $table, _vm, allAlign, allColumnHeaderOverflow, allHeaderAlign, border, columnKey } = args - let { headerCellClassName, headerColumn, headerRowClassName, headerSuffixIconAbsolute } = args - let { highlightCurrentColumn, isDragHeaderSorting, mouseConfig, overflowX, resizable, sortOpts } = args - let { tableColumn, tableLayout, tableListeners } = args - let args1 = { $table, _vm, allAlign, allColumnHeaderOverflow, allHeaderAlign, border, columnKey } - - Object.assign(args1, { headerCellClassName, headerColumn, headerRowClassName, headerSuffixIconAbsolute }) - Object.assign(args1, { highlightCurrentColumn, isDragHeaderSorting, mouseConfig }) - Object.assign(args1, { overflowX, resizable, sortOpts, tableListeners }) - - return h( - 'table', - { - class: 'tiny-grid__header', - style: { tableLayout }, - attrs: { cellspacing: 0, cellpadding: 0, border: 0 }, - ref: 'table' - }, - [ - // 列宽 - renderTableColgroup(tableColumn), - // 头部 - renderTableThead(args1) - ] - ) -} - -const documentOnmouseup = function ({ - oldMousemove, - oldMouseup, - column, - dragPosLeft, - dragLeft, - resizeBarElem, - $table, - params -}) { - document.onmousemove = oldMousemove - document.onmouseup = oldMouseup - - let resizeWidth = column.renderWidth + dragLeft - dragPosLeft - resizeWidth = typeof resizeWidth === 'number' ? resizeWidth : parseInt(resizeWidth, 10) || 40 - column.resizeWidth = resizeWidth < 40 ? 40 : resizeWidth - - resizeBarElem.style.display = 'none' - removeClass($table.$el, 'tiny-grid-cell__resize') - Object.assign($table, { _isResize: false, _lastResizeTime: Date.now() }) - - $table.analyColumnWidth() - $table.recalculate().then(() => { - // 拖拽后,需要同步表头的scrollLeft - const { tableBody, tableFooter, tableHeader } = $table.$refs || {} - const headerElm = tableHeader?.$el - const bodyElm = tableBody?.$el - const footerElm = tableFooter?.$el - if (!headerElm) { - return - } - const elemStore = $table.elemStore - if (bodyElm) { - bodyElm.scrollLeft = headerElm.scrollLeft - } - if (footerElm) { - footerElm.scrollLeft = headerElm.scrollLeft - } - - if (!elemStore['main-header-repair']) { - return - } - elemStore['main-body-xSpace'].style.width = elemStore['main-header-repair'].style.width - if (elemStore['main-footer-xSpace']) { - elemStore['main-footer-xSpace'].style.width = elemStore['main-header-repair'].style.width - } - }) - updateResizableToolbar($table) - emitEvent($table, 'resizable-change', [params]) -} - -export default defineComponent({ - name: `${$prefix}GridHeader`, - props: { - collectColumn: Array, - fixedColumn: Array, - size: String, - isGroup: Boolean, - tableColumn: Array, - tableData: Array, - visibleColumn: Array, - resizableConfig: Object - }, - watch: { - tableColumn() { - this.uploadColumn() - } - }, - data() { - return { - headerColumn: [] - } - }, - mounted() { - const { $el, $parent: $table, $refs } = this - const { elemStore, dropConfig } = $table - const keyPrefix = 'main-header-' - - elemStore[`${keyPrefix}wrapper`] = $el - elemStore[`${keyPrefix}table`] = $refs.table - elemStore[`${keyPrefix}colgroup`] = $refs.colgroup - elemStore[`${keyPrefix}list`] = $refs.thead - elemStore[`${keyPrefix}x-space`] = $refs.xSpace - elemStore[`${keyPrefix}repair`] = $refs.repair - - if (dropConfig) { - const { plugin, column = true, scheme } = dropConfig - - if (scheme !== 'v2') { - plugin && column && (this.columnSortable = $table.columnDrop(this.$el)) - } - } - }, - beforeUnmount() { - this.columnSortable && this.columnSortable.destroy() - }, - created() { - this.uploadColumn() - }, - render() { - let { $parent: $table, headerColumn, tableColumn } = this - let { align: allAlign, border, columnKey, headerAlign: allHeaderAlign } = $table - let { headerCellClassName, headerRowClassName, headerSuffixIconAbsolute } = $table - let { highlightCurrentColumn, isDragHeaderSorting, mouseConfig = {}, overflowX } = $table - let { resizable, showHeaderOverflow: allColumnHeaderOverflow } = $table - let { sortOpts, tableLayout, tableListeners } = $table - - let args = { $table, _vm: this, allAlign, allColumnHeaderOverflow, allHeaderAlign, border, columnKey } - - Object.assign(args, { headerCellClassName, headerColumn, headerRowClassName, headerSuffixIconAbsolute }) - Object.assign(args, { highlightCurrentColumn, isDragHeaderSorting, mouseConfig, overflowX, resizable, sortOpts }) - Object.assign(args, { tableColumn, tableLayout, tableListeners }) - - return h( - 'div', - { - class: ['tiny-grid__header-wrapper', 'body__wrapper'] - }, - [ - // 表格主体内容x轴方向虚拟滚动条占位元素,在表头中属于无效元素,待删除 - renderXSpace(), - renderTable(args), - // x轴方向虚拟滚动适配线 - renderRepair() - ] - ) - }, - methods: { - uploadColumn() { - this.headerColumn = this.isGroup ? this.$parent._sliceColumnTree(this.tableColumn) : [this.tableColumn] - }, - resizeMousedown(event, params) { - let { $el, $parent: $table, resizableConfig } = this - let { clientX: dragClientX, target: dragBtnElem } = event - let { column } = params - let { dragLeft = 0, minInterval = 36, fixedOffsetWidth = 0 } = {} - let { resizeBar: resizeBarElem, tableBody } = $table.$refs - let { cell = dragBtnElem.parentNode, dragBtnWidth = dragBtnElem.clientWidth } = {} - let startColumnLeft = cell.offsetLeft - let dragBtnOffsetWidth = Math.floor(dragBtnWidth / 2) - const tableBodyElem = tableBody.$el - const btnLeft = dragBtnElem?.getBoundingClientRect().left - $el?.getBoundingClientRect().left - let dragMinLeft = btnLeft - cell.clientWidth + dragBtnWidth + minInterval - let dragPosLeft = btnLeft + dragBtnOffsetWidth - let { oldMousemove = document.onmousemove, oldMouseup = document.onmouseup } = {} - - // 处理拖动事件 - let handleMousemoveEvent = function (event) { - event.stopPropagation() - event.preventDefault() - - let { offsetX = event.clientX - dragClientX, left = offsetX + dragPosLeft } = {} - let scrollLeft = tableBodyElem.scrollLeft - let args = { - cell, - dragMinLeft, - dragPosLeft, - fixedOffsetWidth, - resizableConfig, - scrollLeft, - column, - dragBtnOffsetWidth, - startColumnLeft - } - Object.assign(args, { left, minInterval, tableBodyElem }) - - let ret = computeDragLeft(args) - dragMinLeft = ret.dragMinLeft - dragLeft = ret.dragLeft - - let currentLeft = ret.dragLeft - scrollLeft - - resizeBarElem.style.left = `${currentLeft}px` - } - - resizeBarElem.style.display = 'block' - addClass($table.$el, 'tiny-grid-cell__resize') - $table._isResize = true - - document.onmousemove = handleMousemoveEvent - document.onmouseup = () => { - documentOnmouseup({ oldMousemove, oldMouseup, column, dragPosLeft, dragLeft, resizeBarElem, $table, params }) - } - handleMousemoveEvent(event) - } - } -}) diff --git a/packages/vue/src/grid/src/keyboard/src/methods.ts b/packages/vue/src/grid/src/keyboard/src/methods.ts index a221cfee15..2630ffe6a5 100644 --- a/packages/vue/src/grid/src/keyboard/src/methods.ts +++ b/packages/vue/src/grid/src/keyboard/src/methods.ts @@ -35,7 +35,7 @@ import { handleCellMousedownEvent } from './utils/triggerCellMousedownEvent' import { handleHeaderCellMousedownEvent } from './utils/triggerHeaderCellMousedownEvent' -import { warn, Formatter } from '../../tools' +import { warn } from '../../tools' const removeCellClass = (bodyRef, clazz) => arrayEach(bodyRef.$el.querySelectorAll('.' + clazz), (elem) => removeClass(elem, clazz)) @@ -59,31 +59,11 @@ const getModify = ({ offsetTop, offsetLeft, cWidth, cHeight }) => { } } -const writeClipboardText = ({ $table, columns, rows }) => { - const { keyboardConfig = {}, isAsyncColumn } = $table +const writeClipboardText = ({ $table, columns, rows, rowNodes }) => { + const { keyboardConfig = {} } = $table const { clipboard = {} } = keyboardConfig const { writeMethod, cellSplit = ',', rowSplit = ';' } = clipboard - const getCellValue = (column, row) => { - let cellValue = '' - - if (isAsyncColumn) { - const format = column.format || {} - - if (format.async === true && format.type === 'enum') { - cellValue = Formatter.enum.call(column, row[column.property]) - } else if (format.async && typeof format.async.fetch === 'function') { - cellValue = row[$table.getAsyncColumnName(column.property)] - } else { - cellValue = row[column.property] - } - } else { - cellValue = row[column.property] - } - - return cellValue || '' - } - if (!clipboard) return let value @@ -93,12 +73,11 @@ const writeClipboardText = ({ $table, columns, rows }) => { } else { const rowValues = [] - rows.forEach((row) => { + rowNodes.forEach((row) => { const cellValues = [] - columns.forEach((column) => { - const cellValue = getCellValue(column, row) - cellValues.push(cellValue) + row.forEach((col) => { + cellValues.push(col && col.innerText) }) rowValues.push(cellValues.join(cellSplit)) @@ -231,7 +210,7 @@ export default { getCell(this, params).then((resCell) => { params.cell = resCell - this.handleSelected(params, event) + this.handleSelected(params, event, true) this.scrollToRow(params.row, params.column, false, { isLeftArrow, isRightArrow, @@ -242,7 +221,7 @@ export default { // 表头按下事件 triggerHeaderCellMousedownEvent(event, params) { let { $el, elemStore, mouseConfig = {}, tableData } = this - let headerList = elemStore['main-header-list'].children + let headerList = elemStore['main-body-headerList'].children let bodyList = elemStore['main-body-list'].children let cell = params.cell let column = params.column @@ -298,11 +277,15 @@ export default { triggerCellMousedownEvent(event, params) { let { $el, editConfig, editStore, elemStore, mouseConfig = {}, visibleColumn } = this let { actived, checked } = editStore + let { excludes: excludeClo = [] } = mouseConfig let { button } = event let { cell, column, row } = params let isLeftBtn = button === 0 let args + if (excludeClo.includes(column.type || column.property)) { + return + } if ( editConfig && (actived.row !== row || !(editConfig.mode === 'cell' && actived.column === column)) && @@ -317,12 +300,12 @@ export default { let isIndex = column.type === 'index' let startCellNode = getCellNodeIndex(cell) - let headerList = elemStore['main-header-list'].children + let headerList = elemStore['main-body-headerList'].children let bodyList = elemStore['main-body-list'].children let cellFirstElementChild = cell.parentNode.firstElementChild let cellLastElementChild = cell.parentNode.lastElementChild let colIndex = Array.from(cell.parentNode.children).indexOf(cell) - let headStart = headerList[0].children[colIndex] + let headStart = headerList?.[0].children[colIndex] args = { $el, _vm: this, bodyList, cell, cellFirstElementChild } Object.assign(args, { cellLastElementChild, headStart, headerList, isIndex, startCellNode }) @@ -354,7 +337,7 @@ export default { } let bodyElem = elemStore['main-body-list'] - let headerElem = elemStore['main-header-list'] + let headerElem = elemStore['main-body-headerList'] if (bodyElem) { let elem = bodyElem.querySelector('.col__selected') @@ -477,9 +460,9 @@ export default { let column = find(visibleColumn, (col) => col.type === 'index') || visibleColumn[0] let selectorColumnId = `.${column.id}` - let headerListElem = elemStore['main-header-list'] - let headerList = headerListElem.children - let cell = headerListElem.querySelector(selectorColumnId) + let headerListElem = elemStore['main-body-headerList'] + let headerList = headerListElem?.children + let cell = headerListElem?.querySelector(selectorColumnId) let bodyList = elemStore['main-body-list'].children let firstTrElem = bodyList[0] let firstCell = firstTrElem.querySelector(selectorColumnId) @@ -492,7 +475,7 @@ export default { getCell(this, params).then((resCell) => { params.cell = resCell - this.handleSelected(params, event) + this.handleSelected(params, event, true) this.handleHeaderChecked( getRowNodes( @@ -546,7 +529,7 @@ export default { this.editStore.titles.rowNodes = rowNodes }, _clearHeaderChecked() { - let headerElem = this.elemStore['main-header-list'] + let headerElem = this.elemStore['main-body-headerList'] if (headerElem) { let eachHandler = (colNode) => removeClass(colNode, 'col__title-checked') @@ -597,7 +580,7 @@ export default { columns = tableColumn.slice(columnIndex, columnIndex + firstRowsLength) rows = tableData.slice(rowIndex, rowIndex + rowNodes.length) - writeClipboardText({ $table: this, columns, rows }) + writeClipboardText({ $table: this, columns, rows, rowNodes }) } arrayEach(rowNodes, (rowNode, rowIndex) => { @@ -662,7 +645,7 @@ export default { let cell = selected.args.cell let bodyList = elemStore['main-body-list'].children - let { rIndex, cIndex } = getCellIndex({ cell, elemStore, bodyList }) + let { rIndex, cIndex } = getCellIndex({ cell, bodyList }) let maxIndex = bodyList.length - 1 let curIndex = rIndex + rows.length - 1 let targetTrElem = bodyList[curIndex > maxIndex ? maxIndex : curIndex] diff --git a/packages/vue/src/grid/src/menu/src/methods.ts b/packages/vue/src/grid/src/menu/src/methods.ts index 661b76266b..5c9571b969 100644 --- a/packages/vue/src/grid/src/menu/src/methods.ts +++ b/packages/vue/src/grid/src/menu/src/methods.ts @@ -100,14 +100,15 @@ export default { }, // 快捷菜单事件处理 handleGlobalContextmenuEvent(event) { - let { ctxMenuOpts, ctxMenuStore, isCtxMenu } = this - let layoutList = ['header', 'body', 'footer'] + const { ctxMenuOpts, ctxMenuStore, isCtxMenu } = this if (!isCtxMenu) { this.closeMenu() this.closeFilter() + return } + if ( ctxMenuStore.visible && this.$refs.ctxWrapper && @@ -117,42 +118,47 @@ export default { return } - for (let i = 0; i < layoutList.length; i++) { - let layout = layoutList[i] - let eventTargetNode = this.getEventTargetNode(event, this.$el, `tiny-grid-${layout}__column`) - let eventParams = { $table: this, columns: this.visibleColumn.slice(0), type: layout } + let eventTargetNode + const eventParams = { $table: this, columns: this.visibleColumn.slice(0) } + + for (let layout of ['header', 'body', 'footer']) { + eventTargetNode = this.getEventTargetNode(event, this.$el, `tiny-grid-${layout}__column`) + eventParams.type = layout if (eventTargetNode.flag) { - let cell = eventTargetNode.targetElem - let column = this.getColumnNode(cell)?.item - if (!column) { - return - } + const cell = eventTargetNode.targetElem + const column = this.getColumnNode(cell).item let typePrefix = `${layout}-` + Object.assign(eventParams, { cell, column, columnIndex: this.getColumnIndex(column) }) if (layout === 'body') { - let row = this.getRowNode(cell.parentNode).item + const row = this.getRowNode(cell.parentNode).item + typePrefix = '' Object.assign(eventParams, { row, rowIndex: this.getRowIndex(row) }) } this.openContextMenu(event, layout, eventParams) emitEvent(this, `${typePrefix}cell-context-menu`, [eventParams, event]) + return } + } - eventTargetNode = this.getEventTargetNode(event, this.$el, `tiny-grid__${layout}-wrapper`) + eventTargetNode = this.getEventTargetNode(event, this.$el, `tiny-grid__body-wrapper`) - if (eventTargetNode.flag) { - if (ctxMenuOpts.trigger === 'cell') { - event.preventDefault() - } else { - this.openContextMenu(event, layout, eventParams) - } - return + if (eventTargetNode.flag) { + if (ctxMenuOpts.trigger === 'cell') { + event.preventDefault() + } else { + eventParams.type = 'body' + this.openContextMenu(event, layout, eventParams) } + + return } + this.closeMenu() this.closeFilter() }, diff --git a/packages/vue/src/grid/src/mobile-first/index.vue b/packages/vue/src/grid/src/mobile-first/index.vue index e714333d58..50d0b081c3 100644 --- a/packages/vue/src/grid/src/mobile-first/index.vue +++ b/packages/vue/src/grid/src/mobile-first/index.vue @@ -45,7 +45,7 @@