From 49072ba5bd236b2f112689d5fd844b00a9e36ca7 Mon Sep 17 00:00:00 2001 From: sheepluo Date: Fri, 11 Feb 2022 20:11:40 +0800 Subject: [PATCH 1/6] =?UTF-8?q?docs(notification):=20=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E4=B8=8E=E5=87=BD=E6=95=B0=E5=BC=8F=E8=B0=83?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/notification/notification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/notification/notification.md b/examples/notification/notification.md index 35af8b00f..012bade20 100644 --- a/examples/notification/notification.md +++ b/examples/notification/notification.md @@ -6,7 +6,7 @@ {{ close-all }} -### 指令调用形式的消息通知 +### 插件调用与函数式调用 支持插件式调用 `this.$notify` 和函数式调用 `NotifyPlugin` 两种方式,两种方式参数完全一样。 From bbe4c10747f3385ae72b88954c1ed25e31c8743c Mon Sep 17 00:00:00 2001 From: chaishi Date: Tue, 29 Mar 2022 21:18:07 +0800 Subject: [PATCH 2/6] fix(table): merged cells border style --- src/_common | 2 +- src/table/hooks/useClassName.ts | 1 + src/table/tr.tsx | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_common b/src/_common index d0706ddbf..199775dd0 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit d0706ddbf12a4b76ed8f322af344af47fe17b4b2 +Subproject commit 199775dd00796684af23086d975670cfdf193801 diff --git a/src/table/hooks/useClassName.ts b/src/table/hooks/useClassName.ts index 689ab043a..5e7ee94c4 100644 --- a/src/table/hooks/useClassName.ts +++ b/src/table/hooks/useClassName.ts @@ -9,6 +9,7 @@ export default function useClassName() { content: `${classPrefix.value}-table__content`, topContent: `${classPrefix.value}-table__top-content`, tdLastRow: `${classPrefix.value}-table__td-last-row`, + tdFirstCol: `${classPrefix.value}-table__td-first-col`, thCellInner: `${classPrefix.value}-table__th-cell-inner`, bordered: `${classPrefix.value}-table--bordered`, striped: `${classPrefix.value}-table--striped`, diff --git a/src/table/tr.tsx b/src/table/tr.tsx index 7e430ae42..838b59436 100644 --- a/src/table/tr.tsx +++ b/src/table/tr.tsx @@ -292,6 +292,7 @@ export default defineComponent({ { [this.tdEllipsisClass]: col.ellipsis, [this.tableBaseClass.tdLastRow]: rowIndex + cellSpans.rowspan === dataLength, + [this.tableBaseClass.tdFirstCol]: colIndex === 0, [this.tdAlignClasses[col.align]]: col.align && col.align !== 'left', }, ]; From 871f1b166c27d6dda63c71f728fee80c099da29f Mon Sep 17 00:00:00 2001 From: chaishi Date: Sat, 21 May 2022 16:52:45 +0800 Subject: [PATCH 3/6] fix(table): toggleData and FoldAll --- examples/table/table.md | 4 ++-- src/common.ts | 6 +++--- src/table/hooks/tree-store.ts | 6 ++++-- src/table/hooks/useTreeData.tsx | 26 +++++++++++++------------- src/table/type.ts | 1 + 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/examples/table/table.md b/examples/table/table.md index 4a07510e2..41a524e61 100644 --- a/examples/table/table.md +++ b/examples/table/table.md @@ -172,14 +172,14 @@ tree | Object | - | 树形结构相关配置。具体属性文档查看 `TableTr treeExpandAndFoldIcon | Function | - | 自定义树形结构展开图标,支持全局配置 `GlobalConfigProvider`。TS 类型:`TNode<{ type: 'expand' | 'fold' }>`。[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N `PrimaryTableProps` | \- | - | 继承 `PrimaryTableProps` 中的全部 API | N onAbnormalDragSort | Function | | TS 类型:`(context: TableAbnormalDragSortContext) => void`
异常拖拽排序时触发,如:树形结构中,非同层级之间的交换。`context.code` 指交换异常错误码,固定值;`context.reason` 指交换异常的原因。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/table/type.ts)。
`interface TableAbnormalDragSortContext { code: number; reason: string }`
| N -onTreeExpandChange | Function | | TS 类型:`(context: TableTreeExpandChangeContext) => void`
树形结构,用户操作引起节点展开或收起时触发,代码操作不会触发。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/table/type.ts)。
`interface TableTreeExpandChangeContext { row: T; rowIndex: number; rowState: TableRowState }`
| N +onTreeExpandChange | Function | | TS 类型:`(context: TableTreeExpandChangeContext) => void`
树形结构,用户操作引起节点展开或收起时触发,代码操作不会触发。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/table/type.ts)。
`interface TableTreeExpandChangeContext { row: T; rowIndex: number; rowState: TableRowState; trigger?: 'expand-fold-icon' }`
| N ### EnhancedTable Events 名称 | 参数 | 描述 -- | -- | -- abnormal-drag-sort | `(context: TableAbnormalDragSortContext)` | 异常拖拽排序时触发,如:树形结构中,非同层级之间的交换。`context.code` 指交换异常错误码,固定值;`context.reason` 指交换异常的原因。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/table/type.ts)。
`interface TableAbnormalDragSortContext { code: number; reason: string }`
-tree-expand-change | `(context: TableTreeExpandChangeContext)` | 树形结构,用户操作引起节点展开或收起时触发,代码操作不会触发。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/table/type.ts)。
`interface TableTreeExpandChangeContext { row: T; rowIndex: number; rowState: TableRowState }`
+tree-expand-change | `(context: TableTreeExpandChangeContext)` | 树形结构,用户操作引起节点展开或收起时触发,代码操作不会触发。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/table/type.ts)。
`interface TableTreeExpandChangeContext { row: T; rowIndex: number; rowState: TableRowState; trigger?: 'expand-fold-icon' }`
### EnhancedTableInstanceFunctions 组件实例方法 diff --git a/src/common.ts b/src/common.ts index 4635583ec..0bdd212af 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,4 +1,4 @@ -/** Vue2 特有全局变量 */ +/** Vue2 特有全局类型 */ export type TNodeReturnValue = import('vue/types/vnode').ScopedSlotReturnValue; export type TNode = T extends undefined @@ -10,7 +10,7 @@ export type JsxNode = TNodeReturnValue; export type AttachNodeReturnValue = HTMLElement | Element | Document; export type AttachNode = CSSSelector | ((triggerNode?: HTMLElement) => AttachNodeReturnValue); -// 与滚动相关的容器类型,因为 document 上没有 scroll 相关属性, 因此排除document +// 与滚动相关的容器类型,因为 document 上没有 scroll 相关属性, 因此排除 document export type ScrollContainerElement = Window | HTMLElement; export type ScrollContainer = (() => ScrollContainerElement) | CSSSelector; @@ -21,7 +21,7 @@ export type FormSubmitEvent = Event; export interface Styles { [css: string]: string | number; } -/** 通用全局变量 */ +/** 通用全局类型 */ export type OptionData = { label?: string; diff --git a/src/table/hooks/tree-store.ts b/src/table/hooks/tree-store.ts index b4d06d3ba..417699d32 100644 --- a/src/table/hooks/tree-store.ts +++ b/src/table/hooks/tree-store.ts @@ -386,11 +386,13 @@ class TableTreeStore { if (!parentExpanded) { newData.push(item); } + this.treeDataMap.set(rowValue, state); if (children?.length && !originalExpanded) { // 同步更新父元素的展开数量 let tmpParent = parent; - while (tmpParent) { + while (tmpParent?.row) { tmpParent.expandChildrenLength += children.length; + this.treeDataMap.set(tmpParent.id, tmpParent); tmpParent = tmpParent.parent; } // 继续子元素 @@ -419,7 +421,7 @@ class TableTreeStore { } const children = get(item, keys.childrenKey); if (children?.length) { - this.expandAll(children, keys); + this.foldAll(children, keys); } } return newData; diff --git a/src/table/hooks/useTreeData.tsx b/src/table/hooks/useTreeData.tsx index 36356ffbf..0891ff2ba 100644 --- a/src/table/hooks/useTreeData.tsx +++ b/src/table/hooks/useTreeData.tsx @@ -95,19 +95,19 @@ export default function useTreeData(props: TdEnhancedTableProps, context: SetupC * 组件实例方法,展开或收起某一行 * @param p 行数据 */ - function toggleExpandData(p: { row: TableRowData; rowIndex: number; trigger?: 'inner' }) { + function toggleExpandData(p: { row: TableRowData; rowIndex: number }, trigger?: 'expand-fold-icon') { dataSource.value = store.value.toggleExpandData(p, dataSource.value, rowDataKeys.value); - if (p?.trigger === 'inner') { - const rowValue = get(p.row, rowDataKeys.value.rowKey); - const params = { - row: p.row, - rowIndex: p.rowIndex, - rowState: store.value?.treeDataMap?.get(rowValue), - }; - props.onTreeExpandChange?.(params); - // Vue3 ignore next line - context.emit('tree-expand-change', params); - } + const rowValue = get(p.row, rowDataKeys.value.rowKey); + const rowState = store.value?.treeDataMap?.get(rowValue); + const params = { + row: p.row, + rowIndex: p.rowIndex, + rowState, + trigger, + }; + props.onTreeExpandChange?.(params); + // Vue3 ignore next line + context.emit('tree-expand-change', params); } function getTreeNodeColumnCol() { @@ -139,7 +139,7 @@ export default function useTreeData(props: TdEnhancedTableProps, context: SetupC return (
{!!childrenNodes.length && ( - toggleExpandData({ ...p, trigger: 'inner' })}> + toggleExpandData(p, 'expand-fold-icon')}> {iconNode} )} diff --git a/src/table/type.ts b/src/table/type.ts index c1f82fe73..6475bdaac 100644 --- a/src/table/type.ts +++ b/src/table/type.ts @@ -866,6 +866,7 @@ export interface TableTreeExpandChangeContext { row: T; rowIndex: number; rowState: TableRowState; + trigger?: 'expand-fold-icon'; } export type TableRowValue = string | number; From f5bb2e94cdb5f693facb289086792b4ee1144b2d Mon Sep 17 00:00:00 2001 From: chaishi Date: Sat, 21 May 2022 17:48:44 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix(table):=20=E6=A0=91=E5=BD=A2=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E4=B8=AD=EF=BC=8C=E5=8A=A8=E6=80=81=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E8=A1=8C=E9=80=89=E4=B8=AD=E5=88=97=E6=97=B6=EF=BC=8C=E7=A6=81?= =?UTF-8?q?=E7=94=A8=E5=8A=9F=E8=83=BD=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/table/hooks/tree-store.ts | 19 +++++++++++++++++++ src/table/hooks/useTreeData.tsx | 7 +++++++ 2 files changed, 26 insertions(+) diff --git a/src/table/hooks/tree-store.ts b/src/table/hooks/tree-store.ts index 417699d32..54b8f8fc1 100644 --- a/src/table/hooks/tree-store.ts +++ b/src/table/hooks/tree-store.ts @@ -471,6 +471,25 @@ class TableTreeStore { } } + // column.checkProps 和 column.disabled 会影响行的禁用状态,因此当列发生变化时,需要重置禁用状态 + updateDisabledState(dataSource: T[], column: PrimaryTableCol, keys: KeysType) { + for (let i = 0, len = dataSource.length; i < len; i++) { + const item = dataSource[i]; + const rowValue = get(item, keys.rowKey); + if (rowValue === undefined) { + log.error('EnhancedTable', '`rowKey` could be wrong, can not get rowValue from `data` by `rowKey`.'); + return; + } + const state = this.treeDataMap.get(rowValue); + state.disabled = isRowSelectedDisabled(column, item, i); + this.treeDataMap.set(rowValue, state); + const children = get(item, keys.childrenKey); + if (children?.length) { + this.updateDisabledState(children, column, keys); + } + } + } + /** * 校验数据合法性 */ diff --git a/src/table/hooks/useTreeData.tsx b/src/table/hooks/useTreeData.tsx index 0891ff2ba..e4e608465 100644 --- a/src/table/hooks/useTreeData.tsx +++ b/src/table/hooks/useTreeData.tsx @@ -28,6 +28,13 @@ export default function useTreeData(props: TdEnhancedTableProps, context: SetupC childrenKey: props.tree?.childrenKey || 'children', })); + const checkedColumn = computed(() => columns.value.find((col) => col.colKey === 'row-select')); + + watch(checkedColumn, (column) => { + if (!store.value) return; + store.value.updateDisabledState(dataSource.value, column, rowDataKeys.value); + }); + function getFoldIcon(h: CreateElement) { const params = { type: 'fold' }; const defaultFoldIcon = t(global.value.treeExpandAndFoldIcon, h, params) || ; From fb510cecbaac6150c5b65300d26bcea855b0443a Mon Sep 17 00:00:00 2001 From: chaishi Date: Sun, 29 May 2022 17:44:56 +0800 Subject: [PATCH 5/6] feat(table): support edit table cell --- examples/table/demos/editable-cell.vue | 153 ++++++++++++++++ examples/table/table.md | 24 ++- src/date-picker/panel/date-range.tsx | 3 +- src/date-picker/panel/date.tsx | 3 +- src/table/editable-cell.tsx | 239 +++++++++++++++++++++++++ src/table/hooks/useClassName.ts | 2 + src/table/hooks/useEditableCell.tsx | 18 ++ src/table/primary-table.tsx | 8 + src/table/type.ts | 65 ++++++- 9 files changed, 504 insertions(+), 11 deletions(-) create mode 100644 examples/table/demos/editable-cell.vue create mode 100644 src/table/editable-cell.tsx create mode 100644 src/table/hooks/useEditableCell.tsx diff --git a/examples/table/demos/editable-cell.vue b/examples/table/demos/editable-cell.vue new file mode 100644 index 000000000..aef90bbf8 --- /dev/null +++ b/examples/table/demos/editable-cell.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/examples/table/table.md b/examples/table/table.md index 41a524e61..d839a949f 100644 --- a/examples/table/table.md +++ b/examples/table/table.md @@ -155,6 +155,7 @@ checkProps | Object / Function | - | 透传参数,`colKey` 值为 `row-select` children | Array | - | 用于多级表头,泛型 T 指表格数据类型。TS 类型:`Array>` | N colKey | String | - | 渲染列所需字段,必须唯一。值为 `row-select` 表示当前列为行选中操作列。值为 `drag` 表示当前列为拖拽排序操作列 | N disabled | Function | - | 是否禁用行选中,`colKey` 值为 `row-select` 时,配置有效。TS 类型:`(options: {row: T; rowIndex: number }) => boolean` | N +edit | Object | - | 可编辑单元格配置项,具体属性参考文档 `TableEditableCellConfig` 描述。TS 类型:`TableEditableCellConfig` | N filter | Object | - | 过滤规则,支持多选(multiple)、单选(single)、输入框(input) 等三种形式。想要自定义过滤组件,可通过 `filter.component` 实现,自定义过滤组件需要包含参数 value 和事件 change。TS 类型:`TableColumnFilter` | N render | Function | - | 自定义表头或单元格,泛型 T 指表格数据类型。TS 类型:`TNode>` `interface PrimaryTableRenderParams extends PrimaryTableCellParams { type: RenderType }`。[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/table/type.ts) | N sorter | Boolean / Function | false | 该列是否支持排序。值为 true 表示该列支持排序;值类型为函数,表示对本地数据 `data` 进行排序,返回值参考 [MDN Array.sort](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)。泛型 T 指表格数据类型。TS 类型:`boolean | SorterFun` `type SorterFun = (a: T, b: T) => number`。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/table/type.ts) | N @@ -185,11 +186,16 @@ tree-expand-change | `(context: TableTreeExpandChangeContext)` | 树形结构 名称 | 参数 | 返回值 | 描述 -- | -- | -- | -- +appendTo | `(key: TableRowValue, newData: T)` | \- | 必需。树形结构中,为当前节点添加子节点。如果 `key` 为空,则表示为根节点添加子节点 expandAll | \- | \- | 必需。展开全部行 foldAll | \- | \- | 必需。折叠全部行 getData | `(key: TableRowValue)` | `TableRowState` | 必需。树形结构中,用于获取行数据所有信息。泛型 `T` 表示行数据类型。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/table/type.ts)。
`type TableRowValue = string | number`
+getTreeNode | \- | `T[]` | 必需。树形结构中,获取完整的树形结构 +insertAfter | `(key: TableRowValue, newData: T)` | \- | 必需。树形结构中,在当前节点之后添加子节点 +insertBefore | `(key: TableRowValue, newData: T)` | \- | 必需。树形结构中,在当前节点之前添加子节点 remove | `(key: TableRowValue)` | \- | 必需。树形结构中,移除指定节点 setData | `(key: TableRowValue, newRowData: T)` | \- | 必需。树形结构中,用于更新行数据。泛型 `T` 表示行数据类型 +swapData | `(params: SwapParams)` | \- | 必需。树形结构中,交换两个节点的顺序。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/table/type.ts)。
`interface SwapParams { current: T; target: T; currentIndex: number; targetIndex: number }`
toggleExpandData | `(p: { row: T, rowIndex: number})` | \- | 必需。展开或收起树形行 ### TableRowState @@ -221,11 +227,11 @@ type | String | - | 用于设置筛选器类型:单选按钮筛选器、复选 名称 | 类型 | 默认值 | 说明 | 必传 -- | -- | -- | -- | -- -bufferSize | Number | 20 | 表示表格除可视区域外,额外渲染的行数,避免表格快速滚动过程中,新出现的内容来不及渲染从而出现空白 | N -isFixedRowHeight | Boolean | false | 表示表格每行内容是否同一个固定高度,仅在 `scroll.type` 为 `virtual` 时有效,该属性设置为 `true` 时,可用于简化虚拟滚动内部计算逻辑,提升性能,此时则需要明确指定 `scroll.rowHeight` 属性的值 | N -rowHeight | Number | - | 表格的行高,不会给``元素添加样式高度,仅作为滚动时的行高参考。一般情况不需要设置该属性。如果设置,可尽量将该属性设置为表格每行平均高度,从而使得表格滚动过程更加平滑 | N +bufferSize | Number | 20 | 表示除可视区域外,额外渲染的行数,避免快速滚动过程中,新出现的内容来不及渲染从而出现空白 | N +isFixedRowHeight | Boolean | false | 表示每行内容是否同一个固定高度,仅在 `scroll.type` 为 `virtual` 时有效,该属性设置为 `true` 时,可用于简化虚拟滚动内部计算逻辑,提升性能,此时则需要明确指定 `scroll.rowHeight` 属性的值 | N +rowHeight | Number | - | 行高,不会给``元素添加样式高度,仅作为滚动时的行高参考。一般情况不需要设置该属性。如果设置,可尽量将该属性设置为每行平均高度,从而使得滚动过程更加平滑 | N threshold | Number | 100 | 启动虚拟滚动的阈值。为保证组件收益最大化,当数据量小于阈值 `scroll.threshold` 时,无论虚拟滚动的配置是否存在,组件内部都不会开启虚拟滚动 | N -type | String | - | 必需。表格滚动加载类型,有两种:懒加载和虚拟滚动。
值为 `lazy` ,表示表格滚动时会进行懒加载,非可视区域内的表格内容将不会默认渲染,直到该内容可见时,才会进行渲染,并且已渲染的内容滚动到不可见时,不会被销毁;
值为`virtual`时,表示表格会进行虚拟滚动,无论滚动条滚动到哪个位置,同一时刻,表格仅渲染该可视区域内的表格内容,当表格需要展示的数据量较大时,建议开启该特性。可选项:lazy/virtual | Y +type | String | - | 必需。滚动加载类型,有两种:懒加载和虚拟滚动。
值为 `lazy` ,表示滚动时会进行懒加载,非可视区域内的内容将不会默认渲染,直到该内容可见时,才会进行渲染,并且已渲染的内容滚动到不可见时,不会被销毁;
值为`virtual`时,表示会进行虚拟滚动,无论滚动条滚动到哪个位置,同一时刻,仅渲染该可视区域内的内容,当需要展示的数据量较大时,建议开启该特性。可选项:lazy/virtual | Y ### TableColumnController @@ -239,6 +245,16 @@ fields | Array | - | 用于设置允许用户对哪些列进行显示或隐藏 hideTriggerButton | Boolean | false | 是否隐藏表格组件内置的“列配置”按钮 | N placement | String | top-right | 列配置按钮基于表格的放置位置:左上角、右上角、左下角、右下角等。可选项:top-left/top-right/bottom-left/bottom-right | N +### TableEditableCellConfig + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +abortEditOnEvent | Array | - | 除了点击非自身元素退出编辑态之外,还有哪些事件退出编辑态。示例:`abortEditOnEvent: ['onChange']`。TS 类型:`string[]` | N +component | \- | - | 组件定义,如:`Input` `Select`。对于完全自定义的组件(非组件库内的组件),组件需要支持 `value` 和 `onChange` ;如果还需要支持校验规则,则组件还需实现 `tips` 和 `status` 两个 API,实现规则可参考 `Input` 组件。TS 类型:`ComponentType`。[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N +onEdited | Function | - | 编辑完成后,退出编辑模式时触发。TS 类型:`(context: { trigger: string; newRowData: T; rowIndex: number }) => void` | N +props | Object | - | 透传给组件 `edit.component` 的属性。TS 类型:`{ [key: string]: any }` | N +rules | Array | - | 校验规则。TS 类型:`FormRule[]`,[Form API Documents](./form?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/table/type.ts) | N + ### TableTreeConfig 名称 | 类型 | 默认值 | 说明 | 必传 diff --git a/src/date-picker/panel/date-range.tsx b/src/date-picker/panel/date-range.tsx index 359876b25..5b64c78a2 100644 --- a/src/date-picker/panel/date-range.tsx +++ b/src/date-picker/panel/date-range.tsx @@ -282,10 +282,11 @@ export default Vue.extend +
e.stopPropagation()}>
({ }, render() { return ( -
+ // 去除非必要的事件冒泡,方便外层处理相关交互。如:可编辑单元格的表格 +
e.stopPropagation()}> ; + colIndex: number; + oldCell: PrimaryTableCol['cell']; +} + +export default defineComponent({ + name: 'TableEditableCell', + props: { + row: Object as PropType, + rowIndex: Number, + col: Object as PropType, + colIndex: Number, + oldCell: [Function, String] as PropType, + }, + + setup(props: EditableCellProps, context: SetupContext) { + const { row, col } = toRefs(props); + const { tableBaseClass } = useClassName(); + const tableEditableCellRef = ref(null); + const isEdit = ref(false); + const editValue = ref(); + const errorList = ref(); + + const currentRow = computed(() => { + const newRow = { ...row.value }; + col.value && set(newRow, col.value.colKey, editValue.value); + return newRow; + }); + + const cellNode = computed(() => { + const node = renderCell( + { + row: currentRow.value, + col: { ...col.value, cell: props.oldCell }, + rowIndex: props.rowIndex, + colIndex: props.colIndex, + }, + context.slots, + ); + return node; + }); + + const componentProps = computed(() => { + const { edit } = col.value; + if (!edit) return {}; + const editProps = { ...edit.props }; + // to remove warn: runtime-core.esm-bundler.js:38 [Vue warn]: Invalid prop: type check failed for prop "onChange". Expected Function, got Array + delete editProps.onChange; + delete editProps.value; + edit.abortEditOnEvent?.forEach((item) => { + delete editProps[item]; + }); + return editProps; + }); + + const isAbortEditOnChange = computed(() => { + const { edit } = col.value; + if (!edit) return false; + return Boolean(edit.abortEditOnEvent?.includes('onChange')); + }); + + const validateEdit = () => new Promise((resolve) => { + if (!col.value.edit?.rules) { + resolve(true); + return; + } + validate(editValue.value, col.value.edit?.rules).then((result) => { + errorList.value = result?.filter((t) => !t.result); + if (!errorList.value || !errorList.value.length) { + resolve(true); + } else { + resolve(errorList); + } + }); + }); + + const isSame = (a: any, b: any) => { + if (typeof a === 'object' && typeof b === 'object') { + return JSON.stringify(a) === JSON.stringify(b); + } + return a === b; + }; + + const updateAndSaveAbort = (outsideAbortEvent: Function, ...args: any) => { + validateEdit().then((result) => { + if (result !== true) return; + // 相同的值无需触发变化 + if (!isSame(editValue.value, get(row.value, col.value.colKey))) { + outsideAbortEvent?.(...args); + } + // 此处必须在事件执行完成后异步销毁编辑组件,否则会导致事件清楚不及时引起的其他问题 + const timer = setTimeout(() => { + isEdit.value = false; + clearTimeout(timer); + }, 0); + }); + }; + + const listeners = computed<{ [key: string]: Function }>(() => { + const { edit } = col.value; + if (!isEdit.value) return; + if (!edit?.abortEditOnEvent?.length) return {}; + // 自定义退出编辑态的事件 + const tListeners = {}; + edit.abortEditOnEvent.forEach((itemEvent) => { + if (itemEvent === 'onChange') return; + const outsideAbortEvent = edit.props[itemEvent]; + tListeners[itemEvent] = (...args: any) => { + updateAndSaveAbort( + outsideAbortEvent, + { + trigger: itemEvent, + newRowData: currentRow.value, + rowIndex: props.rowIndex, + }, + ...args, + ); + }; + }); + + return tListeners; + }); + + const onEditChange = (val: any, ...args: any) => { + editValue.value = val; + if (isAbortEditOnChange.value) { + const outsideAbortEvent = col.value.edit?.onEdited; + updateAndSaveAbort( + outsideAbortEvent, + { + trigger: 'onChange', + newRowData: currentRow.value, + rowIndex: props.rowIndex, + }, + ...args, + ); + } + }; + + const documentClickHandler = (e: PointerEvent) => { + if (!col.value.edit || !col.value.edit.component) return; + if (!isEdit.value || !tableEditableCellRef.value?.$el) return; + // @ts-ignore + if (e.path?.includes(tableEditableCellRef.value?.$el)) return; + const outsideAbortEvent = col.value.edit.onEdited; + updateAndSaveAbort(outsideAbortEvent, { + trigger: 'document', + newRowData: currentRow.value, + rowIndex: props.rowIndex, + }); + }; + + watch( + row, + (row) => { + let val = get(row, col.value.colKey); + if (typeof val === 'object') { + val = val instanceof Array ? [...val] : { ...val }; + } + editValue.value = val; + }, + { immediate: true }, + ); + + watch(isEdit, (isEdit) => { + if (!col.value.edit || !col.value.edit.component) return; + if (isEdit) { + document.addEventListener('click', documentClickHandler); + } else { + document.removeEventListener('click', documentClickHandler); + } + }); + + return { + editValue, + isEdit, + tableBaseClass, + cellNode, + isAbortEditOnChange, + listeners, + componentProps, + tableEditableCellRef, + errorList, + onEditChange, + }; + }, + + render() { + if (!this.isEdit) { + return ( +
{ + this.isEdit = true; + e.stopPropagation(); + }} + > + {this.cellNode} + +
+ ); + } + // @ts-ignore + const component = this.col.edit?.component; + if (!component) { + log.error('Table', 'edit.component is required.'); + return null; + } + const errorMessage = this.errorList?.[0]?.message; + return ( +
+ +
+ ); + }, +}); diff --git a/src/table/hooks/useClassName.ts b/src/table/hooks/useClassName.ts index a5b1cde32..6a100c1e8 100644 --- a/src/table/hooks/useClassName.ts +++ b/src/table/hooks/useClassName.ts @@ -13,6 +13,8 @@ export default function useClassName() { tdLastRow: `${classPrefix.value}-table__td-last-row`, tdFirstCol: `${classPrefix.value}-table__td-first-col`, thCellInner: `${classPrefix.value}-table__th-cell-inner`, + cellEditable: `${classPrefix.value}-table__cell--editable`, + cellEditWrap: `${classPrefix.value}-table__cell-wrap`, bordered: `${classPrefix.value}-table--bordered`, striped: `${classPrefix.value}-table--striped`, hover: `${classPrefix.value}-table--hoverable`, diff --git a/src/table/hooks/useEditableCell.tsx b/src/table/hooks/useEditableCell.tsx new file mode 100644 index 000000000..4afd6de86 --- /dev/null +++ b/src/table/hooks/useEditableCell.tsx @@ -0,0 +1,18 @@ +import { CreateElement } from 'vue'; +import { SetupContext } from '@vue/composition-api'; +import { + TableRowData, PrimaryTableCellParams, TdPrimaryTableProps, PrimaryTableCol, +} from '../type'; +import EditableCell from '../editable-cell'; + +export default function useEditableCell(props: TdPrimaryTableProps, context: SetupContext) { + const renderEditableCell = ( + h: CreateElement, + p: PrimaryTableCellParams, + oldCell: PrimaryTableCol['cell'], + ) => ; + + return { + renderEditableCell, + }; +} diff --git a/src/table/primary-table.tsx b/src/table/primary-table.tsx index 3d87b09be..29f90b3d5 100644 --- a/src/table/primary-table.tsx +++ b/src/table/primary-table.tsx @@ -18,6 +18,7 @@ import useDragSort from './hooks/useDragSort'; import useAsyncLoading from './hooks/useAsyncLoading'; import { PageInfo } from '../pagination'; import useClassName from './hooks/useClassName'; +import useEditableCell from './hooks/useEditableCell'; export { BASE_TABLE_ALL_EVENTS } from './base-table'; @@ -88,6 +89,8 @@ export default defineComponent({ const { renderTitleWidthIcon } = useTableHeader(props); const { renderAsyncLoading } = useAsyncLoading(props, context); + const { renderEditableCell } = useEditableCell(props, context); + const primaryTableClasses = computed(() => ({ [tableDraggableClasses.colDraggable]: isColDraggable.value, [tableDraggableClasses.rowHandlerDraggable]: isRowHandlerDraggable.value, @@ -145,6 +148,11 @@ export default defineComponent({ }; item.ellipsisTitle = false; } + // 如果是单元格可编辑状态 + if (item.edit?.component) { + const oldCell = item.cell; + item.cell = (h, p) => renderEditableCell(h, p, oldCell); + } if (item.children?.length) { item.children = getColumns(item.children); } diff --git a/src/table/type.ts b/src/table/type.ts index 6475bdaac..7bf440fd6 100644 --- a/src/table/type.ts +++ b/src/table/type.ts @@ -16,7 +16,8 @@ import { InputProps } from '../input'; import { ButtonProps } from '../button'; import { CheckboxGroupProps } from '../checkbox'; import { DialogProps } from '../dialog'; -import { TNode, OptionData, SizeEnum, ClassName, HTMLElementAttributes } from '../common'; +import { FormRule } from '../form'; +import { TNode, OptionData, SizeEnum, ClassName, HTMLElementAttributes, ComponentType } from '../common'; export interface TdBaseTableProps { /** @@ -460,6 +461,10 @@ export interface PrimaryTableCol * 是否禁用行选中,`colKey` 值为 `row-select` 时,配置有效 */ disabled?: (options: { row: T; rowIndex: number }) => boolean; + /** + * 可编辑单元格配置项,具体属性参考文档 `TableEditableCellConfig` 描述 + */ + edit?: TableEditableCellConfig; /** * 过滤规则,支持多选(multiple)、单选(single)、输入框(input) 等三种形式。想要自定义过滤组件,可通过 `filter.component` 实现,自定义过滤组件需要包含参数 value 和事件 change */ @@ -514,6 +519,10 @@ export interface TdEnhancedTableProps ext /** 组件实例方法 */ export interface EnhancedTableInstanceFunctions { + /** + * 树形结构中,为当前节点添加子节点。如果 `key` 为空,则表示为根节点添加子节点 + */ + appendTo: (key: TableRowValue, newData: T) => void; /** * 展开全部行 */ @@ -526,6 +535,18 @@ export interface EnhancedTableInstanceFunctions TableRowState; + /** + * 树形结构中,获取完整的树形结构 + */ + getTreeNode: () => T[]; + /** + * 树形结构中,在当前节点之后添加子节点 + */ + insertAfter: (key: TableRowValue, newData: T) => void; + /** + * 树形结构中,在当前节点之前添加子节点 + */ + insertBefore: (key: TableRowValue, newData: T) => void; /** * 树形结构中,移除指定节点 */ @@ -534,6 +555,10 @@ export interface EnhancedTableInstanceFunctions void; + /** + * 树形结构中,交换两个节点的顺序 + */ + swapData: (params: SwapParams) => void; /** * 展开或收起树形行 */ @@ -612,17 +637,17 @@ export interface TableColumnFilter { export interface TableScroll { /** - * 表示表格除可视区域外,额外渲染的行数,避免表格快速滚动过程中,新出现的内容来不及渲染从而出现空白 + * 表示除可视区域外,额外渲染的行数,避免快速滚动过程中,新出现的内容来不及渲染从而出现空白 * @default 20 */ bufferSize?: number; /** - * 表示表格每行内容是否同一个固定高度,仅在 `scroll.type` 为 `virtual` 时有效,该属性设置为 `true` 时,可用于简化虚拟滚动内部计算逻辑,提升性能,此时则需要明确指定 `scroll.rowHeight` 属性的值 + * 表示每行内容是否同一个固定高度,仅在 `scroll.type` 为 `virtual` 时有效,该属性设置为 `true` 时,可用于简化虚拟滚动内部计算逻辑,提升性能,此时则需要明确指定 `scroll.rowHeight` 属性的值 * @default false */ isFixedRowHeight?: boolean; /** - * 表格的行高,不会给``元素添加样式高度,仅作为滚动时的行高参考。一般情况不需要设置该属性。如果设置,可尽量将该属性设置为表格每行平均高度,从而使得表格滚动过程更加平滑 + * 行高,不会给``元素添加样式高度,仅作为滚动时的行高参考。一般情况不需要设置该属性。如果设置,可尽量将该属性设置为每行平均高度,从而使得滚动过程更加平滑 */ rowHeight?: number; /** @@ -631,7 +656,7 @@ export interface TableScroll { */ threshold?: number; /** - * 表格滚动加载类型,有两种:懒加载和虚拟滚动。
值为 `lazy` ,表示表格滚动时会进行懒加载,非可视区域内的表格内容将不会默认渲染,直到该内容可见时,才会进行渲染,并且已渲染的内容滚动到不可见时,不会被销毁;
值为`virtual`时,表示表格会进行虚拟滚动,无论滚动条滚动到哪个位置,同一时刻,表格仅渲染该可视区域内的表格内容,当表格需要展示的数据量较大时,建议开启该特性 + * 滚动加载类型,有两种:懒加载和虚拟滚动。
值为 `lazy` ,表示滚动时会进行懒加载,非可视区域内的内容将不会默认渲染,直到该内容可见时,才会进行渲染,并且已渲染的内容滚动到不可见时,不会被销毁;
值为`virtual`时,表示会进行虚拟滚动,无论滚动条滚动到哪个位置,同一时刻,仅渲染该可视区域内的内容,当需要展示的数据量较大时,建议开启该特性 */ type: 'lazy' | 'virtual'; } @@ -670,6 +695,29 @@ export interface TableColumnController { placement?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; } +export interface TableEditableCellConfig { + /** + * 除了点击非自身元素退出编辑态之外,还有哪些事件退出编辑态。示例:`abortEditOnEvent: ['onChange']` + */ + abortEditOnEvent?: string[]; + /** + * 组件定义,如:`Input` `Select`。对于完全自定义的组件(非组件库内的组件),组件需要支持 `value` 和 `onChange` ;如果还需要支持校验规则,则组件还需实现 `tips` 和 `status` 两个 API,实现规则可参考 `Input` 组件 + */ + component?: ComponentType; + /** + * 编辑完成后,退出编辑模式时触发 + */ + onEdited?: (context: { trigger: string; newRowData: T; rowIndex: number }) => void; + /** + * 透传给组件 `edit.component` 的属性 + */ + props?: { [key: string]: any }; + /** + * 校验规则 + */ + rules?: FormRule[]; +} + export interface TableTreeConfig { /** * 表示树形结构的行选中(多选),父子行选中是否独立 @@ -871,6 +919,13 @@ export interface TableTreeExpandChangeContext { export type TableRowValue = string | number; +export interface SwapParams { + current: T; + target: T; + currentIndex: number; + targetIndex: number; +} + export type FilterProps = RadioProps | CheckboxProps | InputProps | { [key: string]: any }; export type FilterType = 'input' | 'single' | 'multiple'; From 63b69236e281deae4aa321220f53a6257d58da06 Mon Sep 17 00:00:00 2001 From: chaishi Date: Sun, 29 May 2022 17:52:40 +0800 Subject: [PATCH 6/6] test: update snapshots --- test/ssr/__snapshots__/ssr.test.js.snap | 187 ++++++++++++++++++++++++ 1 file changed, 187 insertions(+) diff --git a/test/ssr/__snapshots__/ssr.test.js.snap b/test/ssr/__snapshots__/ssr.test.js.snap index d5da5db85..f0332b6ed 100644 --- a/test/ssr/__snapshots__/ssr.test.js.snap +++ b/test/ssr/__snapshots__/ssr.test.js.snap @@ -14346,6 +14346,193 @@ exports[`ssr snapshot test renders ./examples/table/demos/drag-sort-handler.vue
`; +exports[`ssr snapshot test renders ./examples/table/demos/editable-cell.vue correctly 1`] = ` +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
FirstName
+
+
Framework
+
+
Letters
+
+
Date
+
+
Eric + + + +
+
+
Vue + + + +
+
+
A + + + +
+
+
2021-11-01 + + + +
+
+
Gilberta + + + +
+
+
React + + + +
+
+
B、E + + + +
+
+
2021-12-01 + + + +
+
+
Heriberto + + + +
+
+
Miniprogram + + + +
+
+
C + + + +
+
+
2022-01-01 + + + +
+
+
Lazarus + + + +
+
+
Flutter + + + +
+
+
D、G、H + + + +
+
+
2022-02-01 + + + +
+
+
Eric + + + +
+
+
Vue + + + +
+
+
A + + + +
+
+
2021-11-01 + + + +
+
+
+
+
+`; + exports[`ssr snapshot test renders ./examples/table/demos/empty.vue correctly 1`] = `