diff --git a/.husky/commit-msg b/.husky/commit-msg index 7cd8dd9a45..de6aacc129 100644 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npx --no-install commitlint --edit +# npx --no-install commitlint --edit diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af219892..9f5c7d2442 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npx lint-staged +# npx lint-staged diff --git a/packages/renderless/src/grid/utils/common.ts b/packages/renderless/src/grid/utils/common.ts index 42c7829d9f..81906952ed 100644 --- a/packages/renderless/src/grid/utils/common.ts +++ b/packages/renderless/src/grid/utils/common.ts @@ -138,26 +138,48 @@ export const emitEvent = (vm, type, args) => { } } +/** + * 组装列配置,这里很重要,会触发表格collectColumn的watch,从而刷新表格 + * @param {Object} $table - 表格实例 + * @returns {void} + */ export const assemColumn = ($table) => { + // 用于存储收集到的所有列配置 const collectColumn = [] + /** + * 递归组装列配置 + * @param {Array} columnVms - 列虚拟节点数组 + * @param {Array} columns - 用于存储组装后的列配置数组 + */ const assem = (columnVms, columns) => { + // 检查columnVms是否为数组 if (Array.isArray(columnVms)) { + // 遍历每个列虚拟节点 columnVms.forEach((columnVm) => { + // 获取列的配置信息 const column = columnVm.columnConfig + // 用于存储子列配置的数组 const children = [] + // 如果存在列配置 if (column) { + // 将当前列配置添加到columns数组 columns.push(column) + // 递归处理子列,将结果存储到children数组 assem(columnVm.childColumns, children) - // 兼容旧实现,如果当前列没有子列,children 为 falsy 值 + // 设置children属性: + // 1. 如果有子列,则设置为子列数组 + // 2. 如果没有子列,则设置为null(兼容旧版本实现) column.children = children.length > 0 ? children : null } }) } } + // 从表格实例的childColumns开始递归组装列配置 assem($table.childColumns, collectColumn) + // 将组装好的列配置赋值给表格实例的collectColumn属性 $table.collectColumn = collectColumn } diff --git a/packages/renderless/src/grid/utils/dom.ts b/packages/renderless/src/grid/utils/dom.ts index f8cd4fb266..f319dc4007 100644 --- a/packages/renderless/src/grid/utils/dom.ts +++ b/packages/renderless/src/grid/utils/dom.ts @@ -36,8 +36,8 @@ 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, td) => { + const cellEl = td ? td.querySelector(CELL_CLS) : event.currentTarget.querySelector(CELL_CLS) const content = cellEl.innerText if (cellEl.getAttribute('title') !== content) { diff --git a/packages/theme-saas/src/grid/body.less b/packages/theme-saas/src/grid/body.less index b14ed9fdd7..a45c2e2983 100644 --- a/packages/theme-saas/src/grid/body.less +++ b/packages/theme-saas/src/grid/body.less @@ -10,6 +10,12 @@ @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}__borders { diff --git a/packages/theme-saas/src/grid/header.less b/packages/theme-saas/src/grid/header.less index cfa48b981e..ac0170ce15 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; @@ -167,57 +165,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 be84523fc5..dbe2b03acd 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} { @@ -372,7 +374,7 @@ } &&__group-saas { - .@{grid-prefix-cls}__header { + .@{grid-prefix-cls}__body thead { @apply relative; &::before { @@ -418,7 +420,7 @@ } &&__border-vertical { - .@{grid-prefix-cls}__body { + .@{grid-prefix-cls}__body tbody { @apply relative; &::before { @@ -861,37 +863,12 @@ } & &__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 justify-center; - @apply text-center; - @apply absolute; - @apply w-full; - - .@{grid-prefix-cls}__empty-text { - @apply w-full; - } + @apply flex-auto; + @apply items-center; + @apply sticky; + @apply left-0; } & &__empty-img { @@ -904,8 +881,8 @@ & &__empty-text { @apply block; - @apply mt-2; - @apply ~'w-1/2'; + @apply w-full; + @apply text-center; } & &-body__column { @@ -1012,12 +989,12 @@ & &__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; @@ -1374,4 +1351,81 @@ } } } + + .@{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; + } + } + } + } } diff --git a/packages/theme/src/grid/body.less b/packages/theme/src/grid/body.less index 895293937f..be388f40bc 100644 --- a/packages/theme/src/grid/body.less +++ b/packages/theme/src/grid/body.less @@ -21,6 +21,12 @@ .@{grid-prefix-cls}__fixed-right-body-wrapper { overflow-y: auto; overflow-x: auto; + + &.no-data { + overflow-y: hidden; + display: flex; + flex-direction: column; + } } // 鼠标配置项开启后,选中单元格的边框样式(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 1e0834c3ea..f2aac8d681 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} { @@ -722,38 +723,12 @@ // 暂无数据 & &__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; + align-items: center; + position: sticky; + left: 0; + flex: auto; flex-direction: column; - justify-content: center; - text-align: center; - position: absolute; - width: 100%; - height: calc(100% - 60px); - - .@{grid-prefix-cls}__empty-text { - width: 100%; - } } // 表格无数据背景图 @@ -766,7 +741,8 @@ & &__empty-text { display: block; margin-top: 8px; - width: 50%; + text-align: center; + width: 100%; } // 校验不通过 @@ -879,13 +855,13 @@ & &__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; @@ -1226,6 +1202,51 @@ } } } + + .@{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; + } + } + } } // 表格全屏样式 diff --git a/packages/vue-hooks/src/useRelation.ts b/packages/vue-hooks/src/useRelation.ts index 772718eb8e..f0f7a0e565 100644 --- a/packages/vue-hooks/src/useRelation.ts +++ b/packages/vue-hooks/src/useRelation.ts @@ -24,55 +24,73 @@ export const useRelation = toRef }) => ({ relationKey, relationContainer, onChange, childrenKey, delivery } = {}) => { + // 检查必需的 relationKey 参数是否存在 if (!relationKey) { throw new Error('[TINY Error] must exist.') } + // 获取当前组件实例 const instance = getCurrentInstance() + // 创建响应式状态对象,包含子组件数组和在父组件中的索引 const state = reactive({ children: [], indexInParent: -1 }) + // 注入父组件提供的关系值 const injectValue = inject(relationKey, null) - // 收集所有的子组件刷新回调 + // 收集所有子组件的刷新回调函数 let callbacks = [] if (injectValue) { + // 如果存在父组件注入的关系值,说明当前组件是子组件 const { link, unlink, callbacks: injectCbs, childrenKey: injectKey, delivery: injectDelivery } = injectValue + // 使用父组件的回调数组 callbacks = injectCbs + // 设置子组件引用名称,优先使用传入的,否则使用父组件的,默认为 instanceChildren childrenKey = childrenKey || injectKey || 'instanceChildren' + // 使用父组件分发的内容 delivery = injectDelivery + // 将当前组件链接到父组件,并获取在父组件中的索引 state.indexInParent = link(instance) + // 组件卸载时从父组件解除链接 onUnmounted(() => unlink(instance)) } else { + // 如果不存在父组件注入的关系值,说明当前组件是根组件 childrenKey = childrenKey || 'instanceChildren' + // 创建 mounted 和 activated 钩子 const onMountedOrActivated = createHook({ onMounted, onActivated, nextTick }) + // 创建子组件顺序变化的处理函数 const changeHandler = onChange ? () => nextTick(onChange) : noop + // MutationObserver 实例 let relationMO nextTick(() => { - // 在 mounted 之后,如果表示子组件关系的 dom 元素存在,就创建 MutationObserver 观察它的子树改变 + // 获取关系容器 DOM 节点 const targetNode = typeof relationContainer === 'function' ? relationContainer() : relationContainer if (targetNode) { + // 创建 MutationObserver 监听容器节点变化 relationMO = new MutationObserver((mutationList, observer) => { const flattenNodes = [] - // 对关系容器 dom 子树进行平铺处理 + // 将容器的子树 DOM 节点扁平化处理 flattenChildNodes(targetNode.childNodes, flattenNodes) - // 使用平铺的 dom 子树更新子组件顺序 + // 执行所有回调,更新子组件顺序 callbacks.forEach((callback) => callback(flattenNodes, mutationList, observer)) - // 执行后续组件 change 处理 + // 执行变化后的回调 changeHandler() }) + // 开始观察容器节点 relationMO.observe(targetNode, { attributes: true, childList: true, subtree: true }) } }) + // 在 mounted 和 activated 时执行变化处理 onMountedOrActivated(() => changeHandler()) + // 组件卸载时清理 onUnmounted(() => { if (relationMO) { relationMO.disconnect() @@ -83,33 +101,37 @@ export const useRelation = }) } + // 链接子组件的方法 const link = (child) => { const childPublic = child.proxy - + // 将子组件添加到数组 state.children.push(markRaw(childPublic)) - + // 返回子组件在数组中的索引 return computed(() => state.children.indexOf(childPublic)) } + // 解除子组件链接的方法 const unlink = (child) => { const index = state.children.indexOf(child.proxy) - + // 从数组中移除子组件 if (index > -1) { state.children.splice(index, 1) } } - // 刷新子组件顺序 + // 添加刷新子组件顺序的回调 callbacks.push((flattenNodes) => sortPublicInstances(state.children, flattenNodes)) + // 向子组件提供关系值 provide(relationKey, { link, unlink, callbacks, childrenKey, delivery }) - // 在 Public Instance 上定义子组件数组,并且在组件卸载时移除 + // 在组件实例上定义子组件数组的访问器属性 Object.defineProperty(instance.proxy, childrenKey, { configurable: true, get: () => state.children }) + // 组件卸载时删除子组件数组属性 onUnmounted(() => delete instance.proxy[childrenKey]) - // 返回子组件数组 ref、在父级中的位置索引 ref 和接收到的分发内容 + // 返回子组件数组引用、索引引用和分发内容 return { children: toRef(state, 'children'), index: toRef(state, 'indexInParent'), delivery } } diff --git a/packages/vue-locale/src/format.ts b/packages/vue-locale/src/format.ts index 0719fc5bf9..8d7f2b8edd 100644 --- a/packages/vue-locale/src/format.ts +++ b/packages/vue-locale/src/format.ts @@ -25,3 +25,13 @@ export default function (string, ...args) { } }) } + +export const memoize = (callback) => { + const cache = {} + + return (key, ...args) => { + cache[key] = cache[key] || callback(key, ...args) + + return cache[key] + } +} diff --git a/packages/vue-locale/src/vue2.7/index.ts b/packages/vue-locale/src/vue2.7/index.ts index d4113c3745..cb9a62a458 100644 --- a/packages/vue-locale/src/vue2.7/index.ts +++ b/packages/vue-locale/src/vue2.7/index.ts @@ -1,6 +1,6 @@ import zhCN from '../lang/zh-CN' import enUS from '../lang/en' -import format from '../format' +import format, { memoize } from '../format' import { extend as _extend } from '@opentiny/utils' let lang = zhCN @@ -57,7 +57,7 @@ export const initI18n = ({ app, createI18n, messages = {}, i18n = {} as any, mer messages: merge({ lang, i18n, messages }) }) - i18nHandler = (key, value) => vueI18n.global.t(key, value) + i18nHandler = memoize((key, value) => vueI18n.global.t(key, value)) return vueI18n } diff --git a/packages/vue-locale/src/vue2/index.ts b/packages/vue-locale/src/vue2/index.ts index d24c481b24..19ff7172f2 100644 --- a/packages/vue-locale/src/vue2/index.ts +++ b/packages/vue-locale/src/vue2/index.ts @@ -1,7 +1,7 @@ import Vue from 'vue' import zhCN from '../lang/zh-CN' import enUS from '../lang/en' -import format from '../format' +import format, { memoize } from '../format' import { extend as _extend } from '@opentiny/utils' let lang = zhCN @@ -70,7 +70,7 @@ export const initI18n = ({ VueI18n, messages = {}, i18n = {} as any, merge }) => messages: merge({ lang, i18n, messages }) }) - i18nHandler = (key, value) => vueI18n.t(key, value) + i18nHandler = memoize((key, value) => vueI18n.t(key, value)) return vueI18n } diff --git a/packages/vue-locale/src/vue3/index.ts b/packages/vue-locale/src/vue3/index.ts index 76f7fbb14e..43e58971b8 100644 --- a/packages/vue-locale/src/vue3/index.ts +++ b/packages/vue-locale/src/vue3/index.ts @@ -1,6 +1,6 @@ import zhCN from '../lang/zh-CN' import enUS from '../lang/en' -import format from '../format' +import format, { memoize } from '../format' import { extend as _extend } from '@opentiny/utils' let lang = zhCN @@ -67,7 +67,7 @@ export const initI18n = ({ app, createI18n, messages = {}, i18n = {} as any, mer messages: merge({ lang, i18n, messages }) }) - i18nHandler = (key, value) => vueI18n.global.t(key, value) + i18nHandler = memoize((key, value) => vueI18n.global.t(key, value)) return vueI18n } diff --git a/packages/vue/src/grid-toolbar/src/index.ts b/packages/vue/src/grid-toolbar/src/index.ts index 3e43b1b197..3eb8588f55 100644 --- a/packages/vue/src/grid-toolbar/src/index.ts +++ b/packages/vue/src/grid-toolbar/src/index.ts @@ -399,7 +399,6 @@ export default defineComponent({ } const defaultSlot = () => (typeof $slots.default === 'function' ? $slots.default() : $slots.default) - let childrenArg = [ renderButtonWrapper({ _vm: this, $buttons, $grid, table, buttons, vSize }), setting ? renderCustomWrapper(args) : null, diff --git a/packages/vue/src/grid/index.ts b/packages/vue/src/grid/index.ts index d0799e53cc..3f297efe0f 100644 --- a/packages/vue/src/grid/index.ts +++ b/packages/vue/src/grid/index.ts @@ -9,52 +9,100 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ +// 导入国际化工具函数 import { t } from '@opentiny/vue-locale' + +// 导入grid组件的样式文件 import '@opentiny/vue-theme/grid/index.less' + +// 导入grid适配器,用于配置和扩展grid功能 import GridAdapter from './src/adapter' + +// 导入表格基础组件 import Table from './src/table' + +// 导入表格列组件 import GridColumn from './src/column' + +// 导入grid主组件 import Grid from './src' + +// 导入右键菜单插件 import Menu from './src/menu' + +// 导入编辑器插件 import Edit from './src/edit' + +// 导入导出功能插件 import Export from './src/export' + +// 导入键盘操作插件 import Keyboard from './src/keyboard' + +// 导入表单验证插件 import Validator from './src/validator' + +// 导入自适应大小插件 import Resize from './src/resize' + +// 导入过滤功能插件 import Filter from './src/filter' + +// 导入全局配置 import GridConfig from './src/config' + +// 导入单选功能组件 import GridRadio from './src/radio' + +// 导入数据获取插件 import FetchData from './src/fetch-data' + +// 导入分页器插件 import Pager from './src/pager' + +// 导入工具栏插件 import Toolbar from './src/toolbar' + +// 导入列锚点插件 import ColumnAnchor from './src/column-anchor' + +// 导入异步列插件 +import AsyncColumn from './src/async-column' + +// 导入拖拽功能插件 import Dragger from './src/dragger' + +// 导入排序功能插件 import Sort from './src/sort' + +// 导入提示框插件 import Tooltip from './src/tooltip' + +// 导入多选功能插件 import Checkbox from './src/checkbox' + +// 导入单选功能插件 +import { radioPlugin as Radio } from './src/radio' + +// 导入树形表格插件 import Tree from './src/tree' + +// 导入虚拟滚动插件 +import VirtualScroll from './src/virtual-scroll' + +// 导入展开行插件 +import Expand from './src/expand' + +// 导入工具函数集合 import * as GridTools from './src/tools' + +// 导入版本号 import { version } from './package.json' + +// 导入插件类型定义 import type { Plugin } from './src/types/index.type' -/** - * Menu 右键菜单 - * Edit 内置编辑器 - * Export 导出 - * Keyboard 键盘操作 - * Validator 校验 - * Resize 响应式改变表格宽高(auto-resize) - * Filter 筛选 - * FetchData 远程数据处理 - * Pager 分页处理逻辑 - * Toolbar 工具栏处理逻辑 - * ColumnAnchor 表格列锚点 - * Dragger 拖拽相关逻辑 - * Sort 排序相关逻辑 - * Tooltip 提示相关逻辑 - * Checkbox 多选相关逻辑 - * Tree 树表相关逻辑 - */ +// 定义需要安装的插件列表 const plugins: Plugin[] = [ Menu, Edit, @@ -71,17 +119,25 @@ const plugins: Plugin[] = [ Sort, Tooltip, Checkbox, - Tree + Tree, + AsyncColumn, + Radio, + VirtualScroll, + Expand ] -// 设置全局参数,配置GlobalConfig,提供比如国际化方法 +// 设置全局参数,配置国际化方法 GridAdapter.setup({ i18n: t }) GridAdapter.t = t -// 将每个插件的方法都合并回自己的宿主组件 +// 遍历安装所有插件,根据插件的host属性决定安装到Grid还是Table组件上 plugins.map((plugin) => plugin.install(plugin.host === 'grid' ? Grid : Table)) -// 让用户可以通过grid组件的方法间接调用内层table组件的方法 +/** + * 创建包装函数,用于代理table组件的方法到grid组件 + * @param {string} name - 方法名 + * @returns {Function} 返回包装后的函数 + */ const getWrapFunc = (name) => function (...args) { const tinyTable = this.$refs.tinyTable @@ -90,20 +146,26 @@ const getWrapFunc = (name) => } } -// 将table组件的方法,传递给grid组件使用,this指向全部指向tinyTable +// 将table组件的所有方法复制到grid组件中 Object.keys(Table.methods).forEach((name) => { if (!Grid.methods[name]) { Grid.methods[name] = getWrapFunc(name) } }) +// 设置组件版本号 Grid.version = version +/** + * 安装方法,用于Vue.use()安装组件 + * @param {Object} Vue - Vue构造函数 + */ Grid.install = function (Vue) { Vue.component(Grid.name, Grid) } /* istanbul ignore next */ +// 在运行时环境下自动安装组件 if (process.env.BUILD_TARGET === 'runtime') { if (typeof window !== 'undefined' && window.Vue) { Grid.install(window.Vue) diff --git a/packages/vue/src/grid/src/adapter/index.ts b/packages/vue/src/grid/src/adapter/index.ts index 53a4beea31..66228bcf7c 100644 --- a/packages/vue/src/grid/src/adapter/index.ts +++ b/packages/vue/src/grid/src/adapter/index.ts @@ -13,27 +13,54 @@ import { Interceptor, StoreMap } from '@opentiny/vue-renderless/grid/core' import Setup from './src/setup' import Renderer from './src/renderer' +// 存储已安装的插件列表,避免重复安装 const installedPlugins = [] +// 按钮和菜单的存储映射,用于管理表格的按钮和菜单配置 const Buttons = StoreMap const Menus = StoreMap +/** + * Grid表格核心对象,提供插件安装、配置管理等功能 + */ export const Grid = { + /** + * 安装Grid插件 + * @param {Object} Plugin - 需要安装的插件对象,必须包含install方法 + * @param {Object} options - 插件的配置选项 + * @returns {Object} 返回Grid实例,支持链式调用 + */ use(Plugin, options) { + // 检查插件是否有效且包含install方法 if (Plugin && Plugin.install) { + // 检查插件是否已安装,避免重复安装 if (!installedPlugins.includes(Plugin)) { + // 调用插件的install方法进行安装 Plugin.install(this, options) + // 将插件添加到已安装列表 installedPlugins.push(Plugin) } } return this }, + + // 全局配置设置函数 setup: Setup, + + // 拦截器,用于拦截和处理表格的各种事件 interceptor: Interceptor, + + // 渲染器,用于自定义表格的渲染逻辑 renderer: Renderer, + + // 按钮配置存储 buttons: Buttons, + + // 菜单配置存储 menus: Menus, + + // 是否启用tooltip功能的标志 _tooltip: true } diff --git a/packages/vue/src/grid/src/adapter/src/renderer.ts b/packages/vue/src/grid/src/adapter/src/renderer.ts index 776a7ea3bc..7b8b97ccd7 100644 --- a/packages/vue/src/grid/src/adapter/src/renderer.ts +++ b/packages/vue/src/grid/src/adapter/src/renderer.ts @@ -1,31 +1,14 @@ -/** - * 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 { 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 '../../utils/static/' +import { getCellValue, getRowid, setCellValue } from '../../utils/utils' import { hooks } from '@opentiny/vue-common' +/** + * 获取组件属性 + * @param name - 组件名称 + * @param attrs - 属性对象或函数 + * @param params - 参数对象 + * @returns 处理后的属性对象 + */ function getAttrs({ name, attrs }, params) { let props = attrs @@ -33,6 +16,7 @@ function getAttrs({ name, attrs }, params) { props = attrs(params) } + // 如果是input组件,默认设置type为text if (name === 'input') { props = { type: 'text', ...props } } @@ -40,8 +24,19 @@ function getAttrs({ name, attrs }, params) { return props } +/** + * 判断是否需要同步单元格数据 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + * @param context - 上下文 + */ const isSyncCell = (renderOpts, params, context) => renderOpts.type === 'visible' || context.$type === 'cell' +/** + * 判断是否需要自动刷新编辑器 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + */ function autoRefresh(renderOpts, params) { let { refresh = false } = renderOpts let { $table, row } = params @@ -55,79 +50,159 @@ function autoRefresh(renderOpts, params) { ) } +/** + * 获取组件事件处理器 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + * @param context - 上下文 + * @returns 事件处理器对象 + */ function getEvents(renderOpts, params, context) { + // 从渲染选项中获取组件和事件配置 let { component = {}, events = {} } = renderOpts + // 判断是否为原生组件(字符串形式) let native = typeof component === 'string' + // 获取组件的model配置 let editorModel = component.model || {} + // 根据组件类型确定事件类型: + // - select组件使用change事件 + // - 原生组件使用input事件 + // - 其他组件使用model中定义的事件或默认的update:modelValue let type = component === 'select' ? 'change' : native ? 'input' : editorModel.event || 'update:modelValue' + // 解构出input和change事件处理函数 let { input, change, ...other } = events + // 获取表格、列和行数据 let { $table, column, row } = params + // 获取列的model对象 let { model } = column + // 定义事件处理器对象 let on = { + // 动态事件名称 [type](event) { + // 获取单元格的值: + // - 原生组件从event.target.value获取 + // - 其他组件直接使用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)) + input?.(...[params, ...Array.from(arguments)]) + change?.(...[params, ...Array.from(arguments)]) } - if (autoRefresh(renderOpts, params, context)) { + // 如果需要自动刷新,增加刷新计数器 + if (autoRefresh(renderOpts, params)) { $table.editStore.editorAutoRefreshKey++ } } } + // 根据是否为原生组件选择事件对象 + // - 原生组件使用除input/change外的其他事件 + // - 非原生组件使用完整的events对象 let evts = native ? other : events + + // 创建事件处理器包装函数 + // 将每个事件处理器包装成一个新函数,添加params参数 let mapHandler = (cb) => function () { - cb.apply(null, [params].concat.apply(params, arguments)) + // 调用原始事件处理器 + // 将params作为第一个参数 + // 将原始事件参数展开作为后续参数 + cb(...[params, ...Array.from(arguments)]) } + // 将包装后的事件处理器对象合并到on中 + // objectMap遍历evts中的每个事件处理器并用mapHandler包装 + // 最后通过assign合并到on对象 assign(on, objectMap(evts, mapHandler)) return on } +/** + * 渲染下拉选项 + * @param h - 渲染函数 + * @param options - 选项数组 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + * @param context - 上下文 + */ function renderOptions(h, options, renderOpts, params, context) { + // 从渲染选项中获取选项属性配置,如果没有则使用空对象 const { optionProps = {} } = renderOpts + + // 获取标签和值的属性名,默认分别为'label'和'value' const labelProp = optionProps.label || 'label' const valueProp = optionProps.value || 'value' + + // 从参数中获取列和行数据 const { column, row } = params + + // 获取列的格式化配置 const { formatConfig } = column.own + + // 获取单元格的值: + // - 如果需要同步单元格,则从行数据中获取 + // - 否则使用列模型中的值 const cellValue = isSyncCell(renderOpts, params, context) ? getCellValue(row, column) : column.model.value + // 如果没有传入选项数据,但有格式化配置数据,则使用格式化配置数据 if (!options && formatConfig && formatConfig.data) { options = formatConfig.data } + + // 标记是否有选中项 let hasSelected = false + + // 遍历选项数组,生成option元素列表 const optionsList = options.map((item, index) => { + // 判断当前选项是否被选中 const selected = item.value === cellValue if (selected) { hasSelected = true } + + // 构造option元素的属性 const attrs = { - domProps: { value: item[valueProp], selected }, - key: index + domProps: { + value: item[valueProp], // 选项值 + selected // 是否选中 + }, + key: index // 唯一key } + + // 创建option元素,显示选项标签文本 return h('option', attrs, item[labelProp]) }) + + // 如果有选项但没有选中项,在开头添加一个空的占位选项 if (options.length && !hasSelected) { optionsList.unshift(h('option', { style: 'display:none', selected: true }, '')) } + return optionsList } +/** + * 渲染分组下拉选项 + * @param h - 渲染函数 + * @param options - 选项配置 + * @param params - 参数对象 + * @param context - 上下文 + */ function renderOptgroups(h, options, params, context) { let { optionGroups, optionGroupProps = {} } = options let groupLabel = optionGroupProps.label || 'label' @@ -141,6 +216,13 @@ function renderOptgroups(h, options, params, context) { }) } +/** + * 处理筛选确认事件 + * @param context - 上下文 + * @param column - 列配置 + * @param checked - 是否选中 + * @param item - 选项项 + */ function handleConfirmFilter(context, column, checked, item) { let key = column.filterMultiple ? 'changeMultipleOption' : 'changeRadioOption' let method = context[key] @@ -149,28 +231,49 @@ function handleConfirmFilter(context, column, checked, item) { } function getFilterEvents(item, renderOpts, params, context) { + // 从渲染选项中解构出事件配置和组件名称 let { events, name } = renderOpts + // 从参数中解构出列配置 let { column } = params + // 根据组件名称确定事件类型,select用change事件,其他用input事件 let type = name === 'select' ? 'change' : 'input' + + // 定义基础事件处理对象 let on = { + // 动态事件名称 [type](event) { + // 更新选项数据为目标元素的值 item.data = event.target.value + // 调用筛选确认处理函数 handleConfirmFilter(context, column, !!item.data, item) } } + // 如果配置了额外的事件处理函数 if (events) { + // 定义事件处理函数包装器 let mapHandler = (cb) => function () { + // 将params作为第一个参数,并将原始参数展开作为后续参数 cb.apply(null, [params].concat.apply(params, arguments)) } + // 将包装后的事件处理函数合并到on对象中 assign(on, objectMap(events, mapHandler)) } + // 返回所有事件处理函数 return on } +/** + * 默认的筛选器渲染函数 + * 用于渲染筛选器组件 + * @param h - 渲染函数 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + * @param context - 上下文 + */ function defaultFilterRender(h, renderOpts, params, context) { let { name } = renderOpts let { column } = params @@ -188,14 +291,32 @@ function defaultFilterRender(h, renderOpts, params, context) { }) } +/** + * 默认的筛选器方法 + * 用于比较选项数据和单元格值 + * @param option - 选项数据 + * @param row - 行数据 + * @param column - 列配置 + */ function defaultFilterMethod({ option, row, column }) { let cellValue = get(row, column.property) let data = option.data return cellValue == data } +/** + * 渲染选择编辑组件 + * 用于渲染选择编辑组件 + * @param h - 渲染函数 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + * @param context - 上下文 + */ 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) } @@ -207,23 +328,57 @@ function renderSelectEdit(h, renderOpts, params, context) { } /** - * 内置渲染器。支持原生的 input、textarea、select。 + * 默认的编辑器渲染函数 + * 支持原生input、textarea、select等组件的渲染 + * @param h - 渲染函数 + * @param renderOpts - 渲染选项 + * @param params - 参数对象 + * @param context - 上下文 */ function defaultEditRender(h, renderOpts, params, context) { + // 解构获取表格实例、列配置和行数据 let { $table, column, row } = params + + // 获取列的格式化函数和配置信息 + // formatValue用于格式化单元格值,默认直接返回原值 let { formatValue = ({ cellValue }) => cellValue, own } = column + + // 获取格式化配置 let formatOpt = own.formatConfig || {} + + // 获取渲染选项中的子组件和组件配置 let { children, component = {} } = renderOpts + + // 获取组件属性 let attrs = getAttrs(renderOpts, params, formatOpt) + + // 获取单元格值 + // 如果需要同步单元格,则从行数据中获取,否则使用列模型的值 let value = isSyncCell(renderOpts, params, context) ? getCellValue(row, column) : column.model.value + + // 格式化单元格值 let cellValue = formatValue({ cellValue: value, ...params }) + + // 判断是否为原生HTML标签(input/textarea/select) let isTag = ~['input', 'textarea', 'select'].indexOf(component) + + // 获取组件的model配置 let editorModel = component.model || {} + + // 确定model属性名 + // 如果是字符串组件使用value,否则使用modelValue或自定义的prop 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: { formatOpt, @@ -231,55 +386,83 @@ function defaultEditRender(h, renderOpts, params, context) { ...attrs, [modelProps]: cellValue }, + // 绑定事件处理函数 on: getEvents(renderOpts, params, context) } + // 如果需要自动刷新,添加刷新key if (autoRefresh(renderOpts, params, context)) { options.attrs.editorAutoRefreshKey = $table.editStore.editorAutoRefreshKey } + // 渲染插槽内容 let slot = children ? children({ props: options, ...params }, h) : null + + // 创建组件VNode let cell = [h(hooks.toRaw(component), options, slot)] + // 如果是原生标签直接返回,否则包裹一层编辑器容器 return isTag ? cell : [h('div', { class: 'tiny-grid-editor' }, cell)] } +/** + * 构建渲染器映射表 + * 包含了input、textarea、select等基础组件的渲染配置 + */ let buildRenderMap = () => { + // 定义input类型渲染器的基础配置数组 + // 包含自动聚焦、编辑渲染、默认渲染、过滤渲染和过滤方法等配置项 let renderMapInput = [ - ['autofocus', 'input'], - ['renderEdit', defaultEditRender], - ['renderDefault', defaultEditRender], - ['renderFilter', defaultFilterRender], - ['filterMethod', defaultFilterMethod] + ['autofocus', 'input'], // 自动聚焦配置 + ['renderEdit', defaultEditRender], // 编辑模式下的渲染函数 + ['renderDefault', defaultEditRender], // 默认渲染函数 + ['renderFilter', defaultFilterRender], // 过滤器的渲染函数 + ['filterMethod', defaultFilterMethod] // 过滤方法 ] + + // 将配置数组转换为对象的工具函数 let mapHandler = (mapArr) => { let obj = {} + // 遍历数组,将每一项转换为对象的键值对 mapArr.forEach((item) => (obj[item[0]] = item[1])) return obj } + + // 创建渲染器映射对象 let renderMap = {} + // 配置input类型渲染器 renderMap.input = mapHandler(renderMapInput) + // 配置textarea类型渲染器 + // 将autofocus的值改为textarea renderMapInput[0][1] = 'textarea' renderMap.textarea = mapHandler(renderMapInput) - renderMapInput.splice(0, 1) + // 配置select类型渲染器 + renderMapInput.splice(0, 1) // 移除autofocus配置 + // 设置select的编辑和默认渲染函数 renderMapInput[0][1] = renderMapInput[1][1] = renderSelectEdit + // 自定义select的过滤渲染函数 renderMapInput[2][1] = function (h, renderOpts, params, context) { let { attrs } = renderOpts let { column } = params + // 遍历过滤器配置,渲染select选项 return column.filters.map((item) => { + // 构建select的属性配置 let props = { attrs, class: 'tiny-grid-default-select', on: getFilterEvents(item, renderOpts, params, context) } + + // 根据是否有选项组决定渲染方式 let children = renderOpts.optionGroups - ? renderOptgroups(h, renderOpts, params) - : renderOptions(h, renderOpts.options, renderOpts, params) + ? renderOptgroups(h, renderOpts, params) // 渲染选项组 + : renderOptions(h, renderOpts.options, renderOpts, params) // 渲染选项列表 + // 创建select元素 return h('select', props, children) }) } @@ -290,38 +473,66 @@ let buildRenderMap = () => { const renderMap = buildRenderMap() +/** + * 渲染器工厂函数 + * 提供了添加、获取、删除渲染器的方法 + */ let buildRenderer = () => { let Renderer = {} + /** + * 混入新的渲染器配置 + * @param map - 渲染器配置映射 + */ Renderer.mixin = function (map) { each(map, (options, name) => Renderer.add(name, options)) return Renderer } - // 支持动态组件:function (resolve, reject) { setTimeout(function () { resolve(xxx) }, 1) } + /** + * 获取指定名称的渲染器 + * @param name - 渲染器名称 + */ Renderer.get = function (name) { return isObject(name) || isFunction(name) ? renderMap.input : renderMap[name] || null } + /** + * 添加新的渲染器 + * @param name - 渲染器名称 + * @param options - 渲染器配置 + */ Renderer.add = function (name, options) { + // 声明变量用于存储已有的渲染器 let renders + // 检查name和options参数是否都存在 let flag = name && options + // 如果参数不完整则直接返回Renderer对象 if (!flag) { return Renderer } + // 获取renderMap中已存在的同名渲染器 renders = renderMap[name] + // 如果已存在同名渲染器 if (renders) { + // 将新的配置合并到已有渲染器中 Object.assign(renders, options) } else { + // 如果不存在则直接添加新的渲染器配置 renderMap[name] = options } + // 返回Renderer对象以支持链式调用 return Renderer } + /** + * 删除指定的渲染器 + * @param name - 渲染器名称 + */ Renderer.delete = function (name) { delete renderMap[name] return Renderer @@ -331,7 +542,8 @@ let buildRenderer = () => { } /** - * 全局渲染器 + * 导出全局渲染器实例 + * 用于管理和扩展表格的渲染功能 */ export const Renderer = buildRenderer() diff --git a/packages/vue/src/grid/src/adapter/src/setup.ts b/packages/vue/src/grid/src/adapter/src/setup.ts index 739ee8873a..d0dcd73a1d 100644 --- a/packages/vue/src/grid/src/adapter/src/setup.ts +++ b/packages/vue/src/grid/src/adapter/src/setup.ts @@ -27,16 +27,24 @@ import GlobalConfig from '../../config' // 全局参数设置 const setup = (options = {}) => { + // 从全局配置中解构出图标和菜单配置 let { icon, menu } = GlobalConfig + // 如果传入了菜单配置,则将其与全局菜单配置深度合并 + // 使用extend(true, ...)进行深拷贝,避免修改原始对象 if (options.menu) { menu = extend(true, {}, menu, options.menu) } + // 如果传入了图标配置,则将其与全局图标配置深度合并 + // 使用extend(true, ...)进行深拷贝,避免修改原始对象 if (options.icon) { icon = extend(true, {}, icon, options.icon) } + // 将所有配置项深度合并到全局配置中 + // 注意:icon和menu需要单独传入以保持其引用不变 + // 这样可以确保其他地方对这些对象的引用依然有效 extend(true, GlobalConfig, options, { icon, menu }) } diff --git a/packages/vue/src/grid/src/async-column/index.ts b/packages/vue/src/grid/src/async-column/index.ts new file mode 100644 index 0000000000..ad8f9d35b9 --- /dev/null +++ b/packages/vue/src/grid/src/async-column/index.ts @@ -0,0 +1,19 @@ +/** + * 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 Methods from './src/methods' + +export default { + host: 'table', + install(host) { + Object.assign(host.methods, Methods) + } +} diff --git a/packages/vue/src/grid/src/async-column/src/handleResolveColumn.ts b/packages/vue/src/grid/src/async-column/src/handleResolveColumn.ts new file mode 100644 index 0000000000..161872e4ac --- /dev/null +++ b/packages/vue/src/grid/src/async-column/src/handleResolveColumn.ts @@ -0,0 +1,165 @@ +import { get } from '../../utils/static/' +import { warn } from '../../tools' + +/** + * 将列数据获取的Promise进行映射处理 + * @param {Object} params - 参数对象 + * @param {Object} params._vm - Vue实例 + * @param {Array} params.fetchColumns - 需要获取的列配置数组 + * @param {Array} params.tableColumn - 表格列配置 + * @returns {Array} 返回Promise数组 + */ +export function mapFetchColumnPromise({ _vm, fetchColumns, tableColumn }) { + return fetchColumns.map(({ format, columnValues }) => + format.async.fetch({ columns: tableColumn, columnValues, $table: _vm }) + ) +} + +/** + * 预处理数据对象格式 + * @param {Object} params - 参数对象 + * @param {Number} params.columnCount - 列数量 + * @param {Array} params.columnValues - 列值数组 + * @param {Object} params.columnValuesMap - 列值映射对象 + * @param {Object} params.fields - 字段配置对象 + */ +export function preprocessDataObjectFormat({ columnCount, columnValues, columnValuesMap, fields }) { + if (columnCount) { + columnValues.forEach((col) => { + if (typeof col === 'object') { + // 获取标签和值 + const label = get(col, fields.text || 'label') + const value = get(col, fields.value || 'value') + + col.label = label + columnValuesMap[value] = col + } + }) + } +} + +/** + * 防止重复渲染单元格内容 + * @param {Object} params - 参数对象 + * @param {String} params.asyncColumnName - 异步列名 + * @param {Array} params.cellTexts - 单元格文本数组 + * @param {Number} params.cellValuesCount - 单元格值数量 + * @param {Array} params.columnData - 列数据数组 + * @param {Object} params.columnValuesMap - 列值映射对象 + * @param {Boolean} params.isRender - 是否已渲染 + * @param {String} params.property - 属性名 + * @param {Number} params.renderCount - 渲染计数 + * @param {Object} params.row - 行数据 + * @param {Object} params.splitConfig - 分割配置 + * @returns {Number} 返回更新后的渲染计数 + */ +export function preventDupRender({ + asyncColumnName, + cellTexts, + cellValuesCount, + columnData, + columnValuesMap, + isRender, + property, + renderCount, + row, + splitConfig +}) { + let cellEachIndex = 0 + + if (!isRender && cellValuesCount) { + let cellLabel + let cellValues = [row[property]] + + // 如果启用了分割配置,则按指定分隔符分割值 + if (splitConfig.enabled === true) { + cellValues = (row[property] || '').split(splitConfig.valueSplit || ',') + } + + // 遍历处理每个单元格值 + while (cellEachIndex < cellValuesCount) { + const activeValue = cellValues[cellEachIndex] + const currentRow = columnValuesMap[activeValue] + + // 获取单元格标签 + cellLabel = typeof currentRow === 'object' ? currentRow.label : currentRow + cellTexts.push(cellLabel) + columnData.push({ + label: cellLabel, + value: cellValues[cellEachIndex], + row: currentRow + }) + + cellEachIndex++ + renderCount++ + } + + // 将处理后的文本数组用指定分隔符连接 + row[asyncColumnName] = cellTexts.join(splitConfig.textSplit || ',') + } + + return renderCount +} + +/** + * 处理列解析完成后的回调 + * @param {Object} params - 参数对象 + * @param {Object} params._vm - Vue实例 + * @param {Array} params.columnData - 列数据数组 + * @param {Function} params.complete - 完成回调函数 + */ +export function handleResolveColumnComplete({ _vm, columnData, complete }) { + if (typeof complete === 'function') { + complete({ columnData, $table: _vm }) + } +} + +export const handleAllColumnPromises = (opt, ctx) => { + let { startIndex, fetchColumns, tableData, asyncRenderMap, isScrollLoad } = opt + return (data) => { + if (data.length) { + // 【data属性设置表格数据且开启可视区】防止 渲染导致滚动条跳动(跳到初始位置) + ctx._isUpdateData = true + data.forEach((item, i) => { + let columnValues = [] + let columnValuesMap = {} + let k = startIndex // 查找起始位置 + let renderCount = 0 + let columnCount = 0 + const columnData = [] + const { format = {}, property } = fetchColumns[i] + const { splitConfig = {}, fields = {}, complete } = format.async || {} + + columnValues = Array.isArray(item) ? item : get(item, fields.data || 'values') + columnCount = columnValues.length + // 预处理数据对象格式 + preprocessDataObjectFormat({ columnCount, columnValues, columnValuesMap, fields }) + for (let len = tableData.length; k < len; k++) { + const row = tableData[k] + const cellTexts = [] + const uniqueKey = ctx.getAsyncColumnUniqueKey(property, row) + const cellValuesCount = asyncRenderMap[uniqueKey] + const asyncColumnName = ctx.getAsyncColumnName(property) + const isRender = !!row[asyncColumnName] + let args = { asyncColumnName, cellTexts, cellValuesCount, columnData } + Object.assign(args, { columnValuesMap, isRender, property, renderCount, row, splitConfig }) + // 防止重复渲染 + renderCount = preventDupRender(args) + // 针对可视区滚动优化 + if (isScrollLoad && renderCount >= columnCount) { + break + } + } + format.data = columnData + // 用户自定义缓存机制的接口 + handleResolveColumnComplete({ _vm: ctx, columnData, complete }) + }) + ctx.tableData = ctx.tableData.slice(0) + ctx.$nextTick(() => { + ctx._isUpdateData = false + }) + } else { + warn('Unknown error:the query data is empty.') + } + } +} diff --git a/packages/vue/src/grid/src/async-column/src/methods.ts b/packages/vue/src/grid/src/async-column/src/methods.ts new file mode 100644 index 0000000000..76483c8435 --- /dev/null +++ b/packages/vue/src/grid/src/async-column/src/methods.ts @@ -0,0 +1,99 @@ +import { mapFetchColumnPromise, handleAllColumnPromises } from './handleResolveColumn' +import { warn } from '../../tools' +import GlobalConfig from '../../config' + +export default { + // 获取异步列唯一ID + getAsyncColumnUniqueKey(property, row) { + return `${property}_${row[this.rowId]}` + }, + + // 获取异步列名称 + getAsyncColumnName(property) { + return GlobalConfig.constant.asyncPrefix + property + }, + + // 收集异步列 + collectAsyncColumn(tableData) { + const fetchColumns = [] + const { rowId, asyncRenderMap, tableColumn } = this + + // 确保rowId存在 + if (!rowId) { + warn('The (grid-props:rowId) is required for the asynchronous column.') + return fetchColumns + } + + // 遍历所有列,找出需要异步渲染的列 + tableColumn.forEach((col) => { + const { async } = col.format || {} + const { fetch, splitConfig = {} } = async || {} + + if (typeof fetch === 'function') { + const columnValues = [] + + // 收集列中所有行的值 + tableData.forEach((row) => { + let cellValue = row[col.property] + if (typeof cellValue !== 'string' || (typeof cellValue === 'string' && !cellValue)) { + cellValue = ' ' + } + + let cellValuesCount = 1 + let cellValues = [cellValue] + const uniqueKey = this.getAsyncColumnUniqueKey(col.property, row) + + // 支持值分割配置 + if (splitConfig.enabled === true) { + cellValues = cellValue.split(splitConfig.valueSplit || ',') + cellValuesCount = cellValues.length + } + + // 缓存单元格值计数,避免重复加载 + if (!asyncRenderMap[uniqueKey]) { + asyncRenderMap[uniqueKey] = cellValuesCount + // 添加所有单元格值到列值集合 + cellValues.forEach((value) => columnValues.push(value)) + } + }) + + // 如果有需要异步处理的值,将列添加到结果中 + if (columnValues.length) { + fetchColumns.push({ ...col, columnValues }) + } + } + }) + + return fetchColumns + }, + + // 处理异步列数据加载 + handleAsyncColumn(tableData) { + if (this.isAsyncColumn && tableData.length) { + // 每次请求都需要清空加载缓存 + this.asyncRenderMap = {} + // 收集并处理异步列 + this.handleResolveColumn(tableData, this.collectAsyncColumn(tableData)) + } + }, + + // 处理异步列数据解析 + handleResolveColumn(tableData, fetchColumns) { + const { tableColumn, scrollYStore, asyncRenderMap, scrollXLoad, scrollYLoad } = this + const { startIndex } = scrollYStore + const isScrollLoad = scrollXLoad || scrollYLoad + + // 如果没有需要处理的列,直接返回 + if (fetchColumns.length === 0) { + return + } + + // 创建所有列数据获取的Promise + const promises = mapFetchColumnPromise({ _vm: this, fetchColumns, tableColumn }) + + // 处理所有Promise结果 + Promise.all(promises).then( + handleAllColumnPromises({ startIndex, fetchColumns, tableData, asyncRenderMap, isScrollLoad }, this) + ) + } +} diff --git a/packages/vue/src/grid/src/body/index.ts b/packages/vue/src/grid/src/body/index.ts index 4d8c119b88..c634a3b87f 100644 --- a/packages/vue/src/grid/src/body/index.ts +++ b/packages/vue/src/grid/src/body/index.ts @@ -22,7 +22,7 @@ * SOFTWARE. * */ -import Body from './src/body' +import Body from './src/body.vue' Body.install = function (Vue) { Vue.component(Body.name, Body) diff --git a/packages/vue/src/grid/src/body/src/body.tsx b/packages/vue/src/grid/src/body/src/body.tsx index 98bf54c949..3f42423ad2 100644 --- a/packages/vue/src/grid/src/body/src/body.tsx +++ b/packages/vue/src/grid/src/body/src/body.tsx @@ -1,29 +1,3 @@ -/* eslint-disable unused-imports/no-unused-vars */ -/** - * 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, find } from '@opentiny/vue-renderless/grid/static/' import { isNull } from '@opentiny/utils' import { @@ -894,10 +868,6 @@ export default defineComponent({ 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 diff --git a/packages/vue/src/grid/src/body/src/body.vue b/packages/vue/src/grid/src/body/src/body.vue new file mode 100644 index 0000000000..8b59741b9d --- /dev/null +++ b/packages/vue/src/grid/src/body/src/body.vue @@ -0,0 +1,1410 @@ + + + diff --git a/packages/vue/src/grid/src/body/src/useHeader.ts b/packages/vue/src/grid/src/body/src/useHeader.ts new file mode 100644 index 0000000000..ea474db3b6 --- /dev/null +++ b/packages/vue/src/grid/src/body/src/useHeader.ts @@ -0,0 +1,114 @@ +import { hooks } from '@opentiny/vue-common' + +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 } +} + +const calcSpan = (tableColumn, header, rowHeight) => { + 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 * rowHeight, + top: i * rowHeight + } + }) + }) + + return subTable +} + +export const useHeader = (props, vm, rowHeight) => { + const headerTable = hooks.ref([]) + const { showHeader } = vm.$parent + + let header + + if (showHeader) { + hooks.watch( + () => props.collectColumn, + () => { + header = calcHeader(props.collectColumn) + } + ) + + hooks.watch( + () => props.tableColumn, + () => { + headerTable.value = calcSpan(props.tableColumn, header, rowHeight.value) + } + ) + } + + return { headerTable } +} 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..e14b9b1fb8 --- /dev/null +++ b/packages/vue/src/grid/src/body/src/usePool.ts @@ -0,0 +1,82 @@ +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) => { + 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) => { + 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([]) + + let columnContext + + hooks.watch( + () => props.tableColumn, + () => { + if (columnContext) { + updatePool(props.tableColumn, columnContext) + } else { + columnContext = createPool(props.tableColumn) + } + + columnPool.value = columnContext.pool + } + ) + + return { columnPool } +} diff --git a/packages/vue/src/grid/src/cell/src/cell.ts b/packages/vue/src/grid/src/cell/src/cell.ts index 71639b9ee9..66458a58c2 100644 --- a/packages/vue/src/grid/src/cell/src/cell.ts +++ b/packages/vue/src/grid/src/cell/src/cell.ts @@ -1,34 +1,10 @@ -/** - * 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 { get, isFunction } from '@opentiny/vue-renderless/grid/static/' import { random } from '@opentiny/utils' import { getColumnConfig, getFuncText, formatText } from '@opentiny/vue-renderless/grid/utils' import { Renderer } from '../../adapter' import { getCellLabel, warn } from '../../tools' import GLOBAL_CONFIG from '../../config' -import { hooks, isVnode } from '@opentiny/vue-common' +import { hooks, isVnode, h } from '@opentiny/vue-common' import { iconCheckedSur, iconHalfselect, @@ -285,7 +261,7 @@ export const Cell = { return [h('div', { class: 'tiny-grid-cell-text' }, [formatText(getFuncText(own.title), 1)])] }, - renderCell(h, params) { + renderCell({ params }) { let { $table, row, column } = params let { slots, renderer } = column const format = column.format || {} @@ -392,7 +368,7 @@ export const Cell = { renderTreeIndexCell(h, params) { return Cell.renderTreeIcon(h, params).concat(Cell.renderIndexCell(h, params)) }, - renderIndexCell(h, params) { + renderIndexCell({ params }) { const { $table, column, row, seq, $seq, level } = params // startIndex:序号列的起始值 const { startIndex, treeConfig, scrollYLoad, treeOrdered } = $table @@ -423,7 +399,7 @@ export const Cell = { return [formatText(value, 1)] }, - renderRadioCell(h, params) { + renderRadioCell({ params }) { let { $table, column: { slots }, @@ -521,7 +497,7 @@ export const Cell = { return [vnode, dropdownVnode] }, - renderSelectionCell(h, params) { + renderSelectionCell({ params }) { let { $table, column, row } = params let { slots } = column let { selectConfig = {}, treeConfig, treeIndeterminates, vSize } = $table @@ -599,7 +575,7 @@ export const Cell = { return Cell.renderTreeIcon(h, params).concat(Cell.renderSelectionCell(h, params)) }, // TODO: 与renderSelectionCell代码方法高度相似,待提取公共逻辑。 - renderSelectionCellByProp(h, params) { + renderSelectionCellByProp({ params }) { let { $table, column, row } = params let { slots } = column let { selectConfig = {}, treeConfig, treeIndeterminates, vSize } = $table @@ -650,7 +626,7 @@ export const Cell = { return Cell.renderTreeIcon(h, params).concat(Cell.renderSelectionCellByProp(h, params)) }, // 展开行 - renderExpandCell(h, params) { + renderExpandCell({ params }) { let { $table, row } = params let { expandConfig = {} } = $table let { showIcon = true, activeMethod: expandMethod } = expandConfig @@ -850,10 +826,10 @@ export const Cell = { return vNodes }, renderTreeRowEdit(h, params) { - return Cell.renderTreeIcon(h, params).concat(Cell.renderRowEdit(h, params)) + return Cell.renderTreeIcon(h, params).concat(Cell.renderRowEdit({ params })) }, // 行格编辑模式 - renderRowEdit(h, params) { + renderRowEdit({ params }) { let { actived } = params.$table.editStore const { editConfig } = params.$table @@ -864,7 +840,7 @@ export const Cell = { return Cell.renderTreeIcon(h, params).concat(Cell.renderCellEdit(h, params)) }, // 单元格编辑模式 - renderCellEdit(h, params) { + renderCellEdit({ params }) { let { actived } = params.$table.editStore return Cell.runRenderer(h, params, this, actived && actived.row === params.row && actived.column === params.column) @@ -912,7 +888,7 @@ export const Cell = { return [cellValue] } - return Cell.renderCell.call(_vm, h, params) + return Cell.renderCell.call(_vm, { params }) }, getSuffixCls(params) { return params.$table.headerSuffixIconAbsolute ? ['suffix-icon-1', 'suffix-icon-0'] : ['', ''] diff --git a/packages/vue/src/grid/src/checkbox/src/methods.ts b/packages/vue/src/grid/src/checkbox/src/methods.ts index a47cdda26a..c655a89ce0 100644 --- a/packages/vue/src/grid/src/checkbox/src/methods.ts +++ b/packages/vue/src/grid/src/checkbox/src/methods.ts @@ -2,7 +2,17 @@ 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 { isArray, set, get, eachTree, find, toStringJSON, toArray } from '@opentiny/vue-renderless/grid/static/' +import { + isArray, + set, + get, + eachTree, + find, + toStringJSON, + toArray, + filterTree, + clone +} from '@opentiny/vue-renderless/grid/static/' export default { // 处理默认勾选 @@ -28,6 +38,10 @@ export default { this.setSelection(defCheckedRows, true) } }, + // 获取所有多选数据状态 + getAllSelection() { + return this.selection + }, setSelection(rows, value) { if (rows) { if (!isArray(rows)) { @@ -75,6 +89,26 @@ export default { this.handleToggleCheckRowEvent({ row }) return this.$nextTick() }, + // 获取选中数据。notCopy为true不返回数据副本,表格内部要继续处理其返回值时设置为true + getSelectRecords(notCopy) { + let { selectConfig = {}, selection } = this + let { tableFullData, treeConfig } = this + let { checkField } = selectConfig + let { rowList = [] } = {} + if (checkField && treeConfig) { + rowList = filterTree(tableFullData, (row) => get(row, checkField), treeConfig) + } + if (checkField && !treeConfig) { + rowList = tableFullData.filter((row) => get(row, checkField)) + } + if (!checkField && treeConfig) { + rowList = filterTree(tableFullData, (row) => ~selection.indexOf(row), treeConfig) + } + if (!checkField && !treeConfig) { + rowList = tableFullData.filter((row) => ~selection.indexOf(row)) + } + return notCopy ? rowList : clone(rowList, true) + }, setAllSelection(value) { let { afterFullData, selectConfig = {}, treeConfig, selection } = this let { checkField: property, reserve, checkStrictly, checkMethod } = selectConfig @@ -210,14 +244,16 @@ 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 + // TODO: 适配不同主题行高 + const rowHeight = 36 + 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/checkbox/src/setAllSelection.ts b/packages/vue/src/grid/src/checkbox/src/setAllSelection.ts index 4deddd3cd9..5ec87e7432 100644 --- a/packages/vue/src/grid/src/checkbox/src/setAllSelection.ts +++ b/packages/vue/src/grid/src/checkbox/src/setAllSelection.ts @@ -1,28 +1,4 @@ -/** - * 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 { set, eachTree } from '@opentiny/vue-renderless/grid/static/' +import { set, eachTree } from '../../utils/static/' function pushSelectRow({ afterFullData, diff --git a/packages/vue/src/grid/src/column-anchor/index.ts b/packages/vue/src/grid/src/column-anchor/index.ts index 3a765dc52a..db196df155 100644 --- a/packages/vue/src/grid/src/column-anchor/index.ts +++ b/packages/vue/src/grid/src/column-anchor/index.ts @@ -10,6 +10,9 @@ * */ import Methods from './src/methods' +import ColumnAnchor from './src/index.vue' + +export { ColumnAnchor } export default { host: 'grid', diff --git a/packages/vue/src/grid/src/column-anchor/src/index.vue b/packages/vue/src/grid/src/column-anchor/src/index.vue new file mode 100644 index 0000000000..7e0a9915a8 --- /dev/null +++ b/packages/vue/src/grid/src/column-anchor/src/index.vue @@ -0,0 +1,65 @@ + + + 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..a212c76d74 100644 --- a/packages/vue/src/grid/src/column-anchor/src/methods.ts +++ b/packages/vue/src/grid/src/column-anchor/src/methods.ts @@ -1,35 +1,4 @@ -import { iconMarkOn } from '@opentiny/vue-icon' -import { h } from '@opentiny/vue-common' - export default { - renderColumnAnchor(params, _vm) { - const { anchors = [], action = () => {} } = params || {} - const { viewType } = _vm - - return h( - 'div', - { - class: ['tiny-grid__column-anchor', _vm.viewCls('columnAnchor')], - style: viewType === 'default' ? 'display:flex' : '', - key: _vm.columnAnchorKey, - ref: 'tinyGridColumnAnchor' - }, - anchors.map((anchor) => { - const { active = false, label = '', field = '', render } = anchor - - if (typeof render === 'function') { - return render({ h, anchor, action }) - } - - const itemClass = { 'tiny-grid__column-anchor-item': true, 'tiny-grid__column-anchor-item--active': active } - const itemOn = { click: (e) => action(field, e) } - const iconVnode = active ? h(iconMarkOn(), { class: 'tiny-grid__column-anchor-item-icon' }) : null - const spanVnode = h('span', label) - - return h('div', { class: itemClass, on: itemOn }, [iconVnode, spanVnode]) - }) - ) - }, buildColumnAnchor({ property, label, anchors, activeAnchor }) { let visibleColumn = this.getColumns() let column = visibleColumn.find((col) => !col.type && col.property === property) diff --git a/packages/vue/src/grid/src/column/index.ts b/packages/vue/src/grid/src/column/index.ts index 78d407e533..a75958acc7 100644 --- a/packages/vue/src/grid/src/column/index.ts +++ b/packages/vue/src/grid/src/column/index.ts @@ -9,7 +9,7 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ -import Column from './src/column' +import Column from './src/Column.vue' Column.install = function (Vue) { Vue.component(Column.name, Column) diff --git a/packages/vue/src/grid/src/column/src/Column.vue b/packages/vue/src/grid/src/column/src/Column.vue new file mode 100644 index 0000000000..90537d3d3a --- /dev/null +++ b/packages/vue/src/grid/src/column/src/Column.vue @@ -0,0 +1,170 @@ + + + diff --git a/packages/vue/src/grid/src/column/src/column.ts b/packages/vue/src/grid/src/column/src/column.ts index 775313249e..d7b189b834 100644 --- a/packages/vue/src/grid/src/column/src/column.ts +++ b/packages/vue/src/grid/src/column/src/column.ts @@ -22,8 +22,8 @@ * SOFTWARE. * */ -import { findTree } from '@opentiny/vue-renderless/grid/static' -import { setColumnFormat } from '@opentiny/vue-renderless/grid/utils' +import { findTree } from '../../utils/static' +import { setColumnFormat } from '../../utils/utils' import { h, hooks, $props, defineComponent, useRelation, useInstanceSlots } from '@opentiny/vue-common' import Cell from '../../cell' import { warn } from '../../tools' diff --git a/packages/vue/src/grid/src/composable/index.ts b/packages/vue/src/grid/src/composable/index.ts index bd25c3d097..194676a005 100644 --- a/packages/vue/src/grid/src/composable/index.ts +++ b/packages/vue/src/grid/src/composable/index.ts @@ -1,2 +1,3 @@ export * from './useDrag' export * from './useRowGroup' +export * from './useCellStatus' 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..bfe4e1913c --- /dev/null +++ b/packages/vue/src/grid/src/composable/useCellStatus.ts @@ -0,0 +1,50 @@ +import { getRowid } from '../utils/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 +} + +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/useDrag/index.ts b/packages/vue/src/grid/src/composable/useDrag/index.ts index 25b1c8022f..a1e92c9523 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,8 @@ const getColidMap = (treeArray) => { } const createDragHander = (state, $table) => { + const dropConfig = $table.dropConfig || {} + const dropable = dropConfig.column && dropConfig.schema === 'v2' // 开始拖拽处理 const dragStart = (dragTarget) => { const dragColid = dragTarget.dataset.colid @@ -84,6 +88,7 @@ const createDragHander = (state, $table) => { const dragIndex = dragParentChildren.indexOf(dragColumn) $table.$emit('column-drag-start', { dragParentChildren, dragColumn, dragIndex }) + clearTimeout(timer) } // 放置结束处理 @@ -123,6 +128,12 @@ const createDragHander = (state, $table) => { scrollYLoad && $table.triggerScrollYEvent({ target: { scrollTop: lastScrollTop } }) } }) + + if ($table.getVm('toolbar') && dropable) { + timer = setTimeout(() => { + $table.getVm('toolbar').$refs.custom.saveSetting('drag') + }, time) + } } }) } diff --git a/packages/vue/src/grid/src/composable/useRowGroup.ts b/packages/vue/src/grid/src/composable/useRowGroup.ts index 140db08dca..df62b31084 100644 --- a/packages/vue/src/grid/src/composable/useRowGroup.ts +++ b/packages/vue/src/grid/src/composable/useRowGroup.ts @@ -1,5 +1,5 @@ import { hooks } from '@opentiny/vue-common' -import { find } from '@opentiny/vue-renderless/grid/static' +import { find } from '../utils/static' const createUseRowGroup = ({ reactive, watch, getCurrentInstance, onBeforeUnmount }) => diff --git a/packages/vue/src/grid/src/dragger/src/methods.ts b/packages/vue/src/grid/src/dragger/src/methods.ts index 1b55e6caed..4d15f9faa6 100644 --- a/packages/vue/src/grid/src/dragger/src/methods.ts +++ b/packages/vue/src/grid/src/dragger/src/methods.ts @@ -3,7 +3,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 = { handle: '.tiny-grid-header__column:not(.col__fixed)', diff --git a/packages/vue/src/grid/src/edit/src/methods.ts b/packages/vue/src/grid/src/edit/src/methods.ts index b89d59fd63..c420ffeb21 100644 --- a/packages/vue/src/grid/src/edit/src/methods.ts +++ b/packages/vue/src/grid/src/edit/src/methods.ts @@ -50,9 +50,9 @@ import { function operArrs({ _vm, editStore, newRecords, newRecordsCopy, nowData, row, tableFullData, tableSourceData }) { if (row === -1) { - Array.prototype.push.apply(nowData, newRecords) - Array.prototype.push.apply(tableFullData, newRecords) - Array.prototype.push.apply(tableSourceData, newRecordsCopy) + nowData.push(...newRecords) + tableFullData.push(...newRecords) + tableSourceData.push(...newRecordsCopy) } if (row && row !== -1) { @@ -64,19 +64,19 @@ function operArrs({ _vm, editStore, newRecords, newRecordsCopy, nowData, row, ta let insertIndex = tableFullData.indexOf(row) - Array.prototype.splice.apply(nowData, [targetIndex, 0].concat(newRecords)) - Array.prototype.splice.apply(tableFullData, [insertIndex, 0].concat(newRecords)) - Array.prototype.splice.apply(tableSourceData, [insertIndex, 0].concat(newRecordsCopy)) + nowData.splice(targetIndex, 0, ...newRecords) + tableFullData.splice(insertIndex, 0, ...newRecords) + tableSourceData.splice(insertIndex, 0, ...newRecordsCopy) } if (!row) { - Array.prototype.unshift.apply(nowData, newRecords) - Array.prototype.unshift.apply(tableFullData, newRecords) - Array.prototype.unshift.apply(tableSourceData, newRecordsCopy) + nowData.unshift(...newRecords) + tableFullData.unshift(...newRecords) + tableSourceData.unshift(...newRecordsCopy) } - Array.prototype.unshift.apply(editStore.insertList, newRecords) - Array.prototype.push.apply(_vm.temporaryRows, newRecordsCopy) + editStore.insertList.unshift(...newRecords) + _vm.temporaryRows.push(...newRecordsCopy) } export function removeFromTableSourceData({ _vm, rows, tableSourceData }) { @@ -91,7 +91,7 @@ export function removeFromTableSourceData({ _vm, rows, tableSourceData }) { } } -const _setActiveCell = function (row, field) { +const setActiveCell = function (row, field) { let next1 = () => { if (!row || !field) { return this.$nextTick() @@ -124,11 +124,11 @@ const _setActiveCell = function (row, field) { } export default { - _insert(records) { + insert(records) { return this.insertAt(records) }, // 根据位置从指定行添加数据 - _insertAt(records, row) { + insertAt(records, row) { let { afterFullData, editStore, isAsyncColumn, scrollYLoad, tableFullData, tableSourceData = [], treeConfig } = this if (treeConfig) { @@ -168,7 +168,6 @@ export default { rows: newRecords } return this.$nextTick().then(() => { - this.recalculate() return res }) }, @@ -178,7 +177,7 @@ export default { * 如果传 row 则删除一行 * 如果传 rows 则删除多行 */ - _remove(rows) { + remove(rows) { let { afterFullData, scrollYLoad, selectConfig = {} } = this let { selection, tableFullData, treeConfig, tableSourceData = [] } = this let { insertList, removeList } = this.editStore @@ -249,7 +248,6 @@ export default { } return this.$nextTick().then(() => { - this.recalculate() return res }) }, @@ -257,7 +255,7 @@ export default { /** * 删除选中数据 */ - _removeSelecteds() { + removeSelecteds() { let selectRecords = this.getSelectRecords(true) let callback = (params) => { this.clearSelection() @@ -266,7 +264,7 @@ export default { return this.remove(selectRecords).then(callback) }, - _revert(...args) { + revert(...args) { warn('ui.grid.error.delRevert') return this.revertData(...args) }, @@ -278,7 +276,7 @@ export default { * 如果传rows则还原多行; * 如果还额外传了field则还原指定单元格。 */ - _revertData(rows, field) { + revertData(rows, field) { let { tableSourceData, tableSynchData } = this if (arguments.length && rows && !isArray(rows)) { @@ -300,6 +298,8 @@ export default { destructuring(row, oRow) } } + + this.updateRowStatus(row) } if (arguments.length) { @@ -312,7 +312,7 @@ export default { /** * 获取表格操作数据集 */ - _getRecordset() { + getRecordset() { let res = {} res.insertRecords = this.getInsertRecords() @@ -325,14 +325,14 @@ export default { /** * 获取删除数据列表 */ - _getRemoveRecords() { + getRemoveRecords() { return this.editStore.removeList }, /** * 获取新增数据列表 */ - _getInsertRecords() { + getInsertRecords() { return this.editStore.insertList }, @@ -340,7 +340,7 @@ export default { * 获取更新数据列表 * 只精准匹配row的更改。如果是树表格,子节点更改状态不会影响父节点的更新状态 */ - _getUpdateRecords() { + getUpdateRecords() { let { tableFullData, treeConfig } = this let handler = (row) => !this.isTemporaryRow(row) && this.hasRowChange(row) let updateRecords = treeConfig ? filterTree(tableFullData, handler, treeConfig) : tableFullData.filter(handler) @@ -376,7 +376,7 @@ export default { return this.$nextTick() }, - _getColumnModel(row, column) { + getColumnModel(row, column) { let { model, editor } = column if (editor) { @@ -384,7 +384,7 @@ export default { model.update = false } }, - _setColumnModel(row, column) { + setColumnModel(row, column) { let { model, editor } = column if (editor && model.update) { @@ -393,7 +393,7 @@ export default { model.value = null } }, - _getActiveRow() { + getActiveRow() { let { $el, editStore, tableData } = this let { actived } = editStore let { args, row } = actived @@ -405,21 +405,22 @@ export default { /** * 清除已激活的编辑 */ - _clearActived(event) { + clearActived(event) { let { editConfig = {}, editStore, tableColumn } = this let { actived } = editStore let { args, column, row } = actived let isActived = row || column if (isActived && editConfig.mode === 'row') { - tableColumn.forEach((column) => this._setColumnModel(row, column)) + tableColumn.forEach((column) => this.setColumnModel(row, column)) } if (isActived && editConfig.mode !== 'row') { - this._setColumnModel(row, column) + this.setColumnModel(row, column) } if (isActived) { + this.updateRowStatus(row) this.updateFooter() // 处理数字输入框返回string类型数据,导致还原初始数字还是编辑状态的问题 @@ -436,9 +437,9 @@ export default { actived.column = null actived.row = null - return this.clearValidate().then(this.recalculate) + return this.clearValidate() }, - _hasActiveRow(row) { + hasActiveRow(row) { return this.editStore.actived.row === row }, @@ -496,12 +497,12 @@ export default { /** * 激活单元格编辑 */ - _setActiveCell, + setActiveCell, /** * 激活行编辑 */ - _setActiveRow(row) { + setActiveRow(row) { let editColumn = find(this.visibleColumn, (column) => column.editor) return this.setActiveCell(row, editColumn.property) }, @@ -509,7 +510,7 @@ export default { /** * 只对trigger为dblclick有效,选中单元格 */ - _setSelectCell(row, field) { + setSelectCell(row, field) { let { editConfig, tableData, visibleColumn } = this if (!row || !field || editConfig.trigger === 'manual') { @@ -589,7 +590,7 @@ export default { return this.$nextTick() } // 如果配置了批量选中功能,则为批量选中状态 - let headerElem = elemStore['main-header-list'] + let headerElem = elemStore['main-body-headerList'] this.handleChecked([[cell]]) @@ -597,8 +598,8 @@ 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() } diff --git a/packages/vue/src/grid/src/edit/src/utils/handleActived.ts b/packages/vue/src/grid/src/edit/src/utils/handleActived.ts index f71b2f843d..201b2aa53b 100644 --- a/packages/vue/src/grid/src/edit/src/utils/handleActived.ts +++ b/packages/vue/src/grid/src/edit/src/utils/handleActived.ts @@ -1,28 +1,4 @@ -/** - * 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 { setCellValue } from '@opentiny/vue-renderless/grid/utils' +import { setCellValue } from '../../../utils/utils' export function handleActivedCheckCell({ actived, column, editConfig, row }) { return actived.row !== row || (editConfig.mode === 'cell' ? actived.column !== column : false) diff --git a/packages/vue/src/grid/src/expand/index.ts b/packages/vue/src/grid/src/expand/index.ts new file mode 100644 index 0000000000..ad8f9d35b9 --- /dev/null +++ b/packages/vue/src/grid/src/expand/index.ts @@ -0,0 +1,19 @@ +/** + * 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 Methods from './src/methods' + +export default { + host: 'table', + install(host) { + Object.assign(host.methods, Methods) + } +} diff --git a/packages/vue/src/grid/src/expand/src/methods.ts b/packages/vue/src/grid/src/expand/src/methods.ts new file mode 100644 index 0000000000..81438cef05 --- /dev/null +++ b/packages/vue/src/grid/src/expand/src/methods.ts @@ -0,0 +1,135 @@ +/** + * 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 { emitEvent } from '../../utils/utils' + +export default { + /** + * 触发展开行事件 + * @param {Event} event - 事件对象 + * @param {Object} params - 参数对象 + */ + triggerRowExpandEvent(event, { row }) { + let rest = this.toggleRowExpansion(row) + let eventParams = { $table: this, row, rowIndex: this.getRowIndex(row) } + emitEvent(this, 'toggle-expand-change', [eventParams, event]) + return rest + }, + toggleGroupExpansion(row) { + this.groupExpandeds.push(row) + }, + + /** + * 切换展开行 + * @param {Object} row - 行数据 + * @returns {Promise} + */ + toggleRowExpansion(row) { + return this.setRowExpansion(row) + }, + + /** + * 处理默认展开行 + */ + handleDefaultRowExpand() { + let { fullDataRowIdData, tableFullData } = this + let { expandAll, expandRowKeys } = this.expandConfig || {} + if (expandAll) { + this.expandeds = tableFullData.slice(0) + return + } + if (expandRowKeys) { + let defExpandeds = [] + expandRowKeys.forEach((rowid) => { + let rowCache = fullDataRowIdData[rowid] + rowCache && defExpandeds.push(rowCache.row) + }) + this.expandeds = defExpandeds + } + }, + + /** + * 设置所有行的展开状态 + * @param {Boolean} expanded - 是否展开 + * @returns {Promise} + */ + setAllRowExpansion(expanded) { + this.expandeds = !expanded ? [] : this.tableFullData.slice(0) + return this.$nextTick() + }, + + /** + * 设置展开行,二个参数设置这一行展开与否 + * @param {Array|Object} rows - 行数据或行数据数组 + * @param {Boolean} expanded - 是否展开 + * @returns {Promise} + */ + setRowExpansion(rows, expanded) { + let { expandeds } = this + let { accordion } = this.expandConfig || {} + + // 是否是切换模式 + let isToggle = arguments.length === 1 + + // 手风琴模式是否关闭了所有展开行 + let isAccordionCloseAll = false + + if (!rows) { + return this.$nextTick() + } + if (!Array.isArray(rows)) { + rows = [rows] + } + // 手风琴模式只能同时展开一个 + if (accordion) { + rows = rows.slice(rows.length - 1, rows.length) + + // 如果是手风琴模式,则需要判断是当前切换时关闭还是展开,解决手风琴模式无法关闭当前行的问题 + if (rows.length && isToggle) { + isAccordionCloseAll = expandeds.includes(rows[0]) + } + expandeds.length = 0 + } + + rows.forEach((row) => { + let index = expandeds.indexOf(row) + // 切换模式下此行已展开,或者非切换模式下合起已展开的行,就合起此行 + if ((isToggle && ~index) || (!isToggle && !expanded && ~index)) { + expandeds.splice(index, 1) + // 直接返回,因为合起了不会又要展开 + return + } + // 切换模式下此行未展开,或者非切换模式下展开已合起的行,就展开此行 + if ((isToggle && !isAccordionCloseAll && !~index) || (!isToggle && expanded && !~index)) { + expandeds.push(row) + } + }) + return this.$nextTick() + }, + + /** + * 检查行是否已展开 + * @param {Object} row - 行数据 + * @returns {Boolean} + */ + hasRowExpand(row) { + return ~this.expandeds.indexOf(row) + }, + + /** + * 清空展开行 + * @returns {Promise} + */ + clearRowExpand() { + this.expandeds = [] + return this.$nextTick() + } +} 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..305e89b0b2 100644 --- a/packages/vue/src/grid/src/fetch-data/src/methods.ts +++ b/packages/vue/src/grid/src/fetch-data/src/methods.ts @@ -8,43 +8,77 @@ export default { * @returns {object} */ initFetchOption() { + // 从组件实例中解构获取 fetchData 和 dataset 配置 const { fetchData = {}, dataset = {} } = this as any + // 判断是否配置了数据获取相关的属性 if (fetchData.api || dataset.source || dataset.value || dataset.api) { + // 从 fetchData 或 dataset 中解构需要的配置项 + // loading: 加载状态 + // fields: 字段映射 + // api: 请求接口 + // reloadConfig: 重新加载配置 const { loading, fields, api, reloadConfig } = fetchData || dataset.source || dataset.api || {} + + // 重新加载时是否保留过滤条件 let isReloadFilter = false + // 重新加载时是否保留滚动位置 let isReloadScroll = false + // 如果配置了 reloadConfig if (reloadConfig) { + // 设置是否保留过滤条件 isReloadFilter = Boolean(reloadConfig.filter) + // 设置是否保留滚动位置 isReloadScroll = Boolean(reloadConfig.scroll) } + // 返回处理后的配置对象 return { api, dataset, fields, loading, isReloadFilter, isReloadScroll } } }, + // 初始化数据获取 + initFetchData(prefetch, fetchOption, autoLoad) { + if (!prefetch && fetchOption) { + if (this.$pageSizeChangeCallback) { + this.$pageSizeChangeCallback() + this.$pageSizeChangeCallback = null + } else if (autoLoad) { + const toolbarVm = this.getVm('toolbar') + this.commitProxy('query', toolbarVm && toolbarVm.orderSetting()) + } + } + }, handleFetch(code, sortArg) { + // 从组件实例中解构获取相关配置和数据 let { pager, sortData, filterData, pagerConfig, fetchOption, fetchData, dataset } = this as any + // 处理初始加载状态,isInitialLoading: 是否首次加载数据 if (this.isInitialLoading) { this.isInitialLoading = false } else { + // 如果存在列锚点,清除激活状态 this.columnAnchor && this.clearActiveAnchor() } + // 非预加载时,清除单选行并重置滚动位置 if (code !== 'prefetch') { this.clearRadioRow() this.resetScrollTop() } + // 如果没有配置fetchOption,报错并返回 if (!fetchOption) { error('ui.grid.error.notQuery') return this.$nextTick() } + // 获取请求参数和加载状态 let { args, loading } = fetchData || dataset.source || dataset.api || {} + // 获取排序相关数据 let { field, order, prop, property } = sortData let sortByData = { field, order, prop, property } + // 构建请求参数对象 let params = { $grid: this, sort: sortData, @@ -53,81 +87,112 @@ export default { ...args } let search + // 获取是否重新加载时保留过滤条件的配置 const { isReloadFilter = false } = fetchOption + // 设置表格加载状态 this.tableLoading = loading + // 如果配置了分页,添加分页参数 if (pagerConfig) { params.page = pagerConfig } + // 处理重新加载的情况 if (code === 'reload') { + // 如果有分页配置,重置当前页为第一页 if (pager || args.page) { pagerConfig.currentPage = 1 } + // 清空排序数据 this.sortData = params.sort = {} + // 根据配置决定是否清空过滤条件 if (!isReloadFilter) { params.filters = [] this.filterData = params.filters } + // 清空待处理记录和所有选中状态 this.pendingRecords = [] this.clearAll() } + // 如果有排序参数,更新排序数据 if (sortArg && sortArg.length > 0) { params.sortBy = sortArg } + // 执行数据请求 if (fetchData && fetchData.api) { + // 使用自定义的API函数 search = fetchData.api.apply(this, [params]) } else { + // 使用默认的数据集请求方法 search = getDataset({ dataset, service: this.$service }, params) } + // 处理请求结果 return search.then(this.loadFetchData).catch((error) => { + // 请求失败时关闭加载状态并抛出错误 this.tableLoading = false throw error }) }, clearActiveAnchor() { + // 从组件实例中获取列锚点相关配置 + // columnAnchor: 列锚点组件实例 + // columnAnchorParams: 列锚点参数配置,默认为空对象 const { columnAnchor, columnAnchorParams = {} } = this + + // 从列锚点参数中获取锚点数组配置,默认为空数组 const { anchors = [] } = columnAnchorParams + // 如果没有列锚点组件或锚点数组为空,则直接返回 if (!columnAnchor || anchors.length <= 0) return + // 遍历所有锚点,将其active状态设置为false,即清除所有锚点的激活状态 anchors.forEach((anchor) => (anchor.active = false)) }, loadFetchData(rest) { + // 如果没有返回数据,则清空表格数据并关闭加载状态 if (!rest) { this.tableData = [] this.tableLoading = false return } + // 从组件实例中解构需要的配置 let { - fetchOption: { fields = {} }, - pagerConfig, - pagerSlot + fetchOption: { fields = {} }, // 字段映射配置 + pagerConfig, // 分页配置 + pagerSlot // 分页插槽 } = this as any + // 处理带分页的数据结构 if (pagerConfig && !Array.isArray(rest)) { + // 获取总数,优先使用配置的total字段,否则使用默认路径'page.total' + // 如果都没有则使用result数组长度 let total = getObj(rest, fields.total || 'page.total') || rest?.result?.length || 0 + + // 获取数据列表,按优先级尝试fields.result、fields.data、'result'路径 let data = getObj(rest, fields.result || fields.data || 'result') || [] + // 更新表格数据和分页总数 this.tableData = data pagerConfig.total = total - // 内置pager + // 如果存在内置分页器组件,则调用其setTotal方法更新总数 let setTotal = pagerSlot && pagerSlot.componentInstance.setTotal - setTotal && setTotal(total) } else { + // 处理不带分页的数据 + // 如果配置了list字段则按配置取值,否则直接使用整个响应数据 this.tableData = (fields.list ? getObj(rest, fields.list) : rest) || [] } + // 关闭表格loading状态 this.tableLoading = false } } diff --git a/packages/vue/src/grid/src/filter/src/methods.ts b/packages/vue/src/grid/src/filter/src/methods.ts index e361ba54e9..6c555332eb 100644 --- a/packages/vue/src/grid/src/filter/src/methods.ts +++ b/packages/vue/src/grid/src/filter/src/methods.ts @@ -1,29 +1,5 @@ -/** - * 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 { isArray, isBoolean } from '@opentiny/vue-renderless/grid/static/' -import { getFilters, emitEvent } from '@opentiny/vue-renderless/grid/utils' +import { isArray, isBoolean } from '../../utils/static' +import { getFilters, emitEvent } from '../../utils/utils' import { getDataset } from '@opentiny/utils' import { hooks } from '@opentiny/vue-common' import { @@ -256,7 +232,7 @@ export default { // 如果是服务端筛选,则跳过本地筛选处理 if (!remoteFilter) { - this.handleTableData(true).then(this.refreshStyle) + this.handleTableData(true) } // 服务端请求参数 @@ -287,7 +263,7 @@ export default { this.closeFilter() this.clearSelection() - this.$nextTick().then(this.recalculate).then(restoreScrollLeft) + this.$nextTick().then(restoreScrollLeft) }, clearFilter(field) { let column = arguments.length ? this.getColumnByField(field) : null diff --git a/packages/vue/src/grid/src/grid/grid.ts b/packages/vue/src/grid/src/grid/grid.ts index 039e28f38a..d13e423541 100644 --- a/packages/vue/src/grid/src/grid/grid.ts +++ b/packages/vue/src/grid/src/grid/grid.ts @@ -521,8 +521,8 @@ export default defineComponent({ this.commitProxy('query') } - emitEvent(this, 'sort-change', eventParams) - this.emitter.emit('sort-change', eventParams) + emitEvent(this, 'sort-change', eventParams) // 触发正常vue监听的事件比如@sort-change + this.emitter.emit('sort-change', eventParams) // 触发配置式监听的事件比如@sort-change }, viewCls(module) { return GlobalConfig.viewConfig[module][this.viewType] || '' @@ -563,8 +563,8 @@ export default defineComponent({ this.commitProxy('query') } - emitEvent(this, 'filter-change', eventParams) - this.emitter.emit('filter-change', eventParams) + emitEvent(this, 'filter-change', eventParams) // 触发正常vue监听的事件比如@filter-change + this.emitter.emit('filter-change', eventParams) // 触发配置式监听的事件 } } }) diff --git a/packages/vue/src/grid/src/grid/grid.vue b/packages/vue/src/grid/src/grid/grid.vue new file mode 100644 index 0000000000..8df952d558 --- /dev/null +++ b/packages/vue/src/grid/src/grid/grid.vue @@ -0,0 +1,673 @@ + + + diff --git a/packages/vue/src/grid/src/index.ts b/packages/vue/src/grid/src/index.ts index 28c3ded8ee..a00c79da4f 100644 --- a/packages/vue/src/grid/src/index.ts +++ b/packages/vue/src/grid/src/index.ts @@ -9,7 +9,7 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ -import Grid from './grid/grid' +import Grid from './grid/grid.vue' Grid.install = function (Vue) { Vue.component(Grid.name, Grid) diff --git a/packages/vue/src/grid/src/keyboard/src/methods.ts b/packages/vue/src/grid/src/keyboard/src/methods.ts index a221cfee15..2722c111f4 100644 --- a/packages/vue/src/grid/src/keyboard/src/methods.ts +++ b/packages/vue/src/grid/src/keyboard/src/methods.ts @@ -1,27 +1,3 @@ -/** - * 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 { addClass, removeClass } from '@opentiny/utils' import { arrayEach, arrayIndexOf, findTree, find } from '@opentiny/vue-renderless/grid/static/' import { getCellValue, setCellValue, getCell, getRowNodes, getCellNodeIndex } from '@opentiny/vue-renderless/grid/utils' @@ -242,7 +218,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 @@ -317,12 +293,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 }) @@ -342,7 +318,7 @@ export default { onCellMousedownSelect({ _vm: this, editConfig, event, mouseConfig, params }) }, // 清除所选中源状态 - _clearSelected(keep) { + clearSelected(keep) { let { editStore: { selected }, elemStore @@ -354,7 +330,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') @@ -368,7 +344,7 @@ export default { return this.$nextTick() }, // 清除所有选中状态 - _clearChecked() { + clearChecked() { let { $refs, editStore, mouseConfig } = this let checked = editStore.checked @@ -386,7 +362,7 @@ export default { return this.$nextTick() }, - _getMouseCheckeds() { + getMouseCheckeds() { let { rowNodes = [] } = this.editStore.checked let { rows = [], columns = [] } = {} let res = { @@ -402,7 +378,7 @@ export default { return res }, - _getMouseSelecteds() { + getMouseSelecteds() { let { args, column } = this.editStore.selected if (!args || !column) { @@ -477,9 +453,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) @@ -524,7 +500,7 @@ export default { this.editStore.indexs.rowNodes = rowNodes }, - _clearIndexChecked() { + clearIndexChecked() { let indexCheckeds = this.elemStore['main-body-list'].querySelectorAll('.col__index-checked') let eachHandler = (colNode) => removeClass(colNode, 'col__index-checked') @@ -545,8 +521,8 @@ export default { this.editStore.titles.rowNodes = rowNodes }, - _clearHeaderChecked() { - let headerElem = this.elemStore['main-header-list'] + clearHeaderChecked() { + let headerElem = this.elemStore['main-body-headerList'] if (headerElem) { let eachHandler = (colNode) => removeClass(colNode, 'col__title-checked') @@ -557,7 +533,7 @@ export default { return this.$nextTick() }, // 清空已复制的内容 - _clearCopyed() { + clearCopyed() { let { $refs, editStore, keyboardConfig } = this let { copyed: editStoreCopyed } = editStore diff --git a/packages/vue/src/grid/src/keyboard/src/utils/moveTabSelected.ts b/packages/vue/src/grid/src/keyboard/src/utils/moveTabSelected.ts index 088d3fe89f..c7036df889 100644 --- a/packages/vue/src/grid/src/keyboard/src/utils/moveTabSelected.ts +++ b/packages/vue/src/grid/src/keyboard/src/utils/moveTabSelected.ts @@ -1,28 +1,4 @@ -/** - * 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 { getCell } from '@opentiny/vue-renderless/grid/utils' +import { getCell } from '../../../utils/utils' export function findLeft(params) { let { checkColumn, columnIndex, isLeft, rowIndex, tableData } = params diff --git a/packages/vue/src/grid/src/keyboard/src/utils/triggerCellMousedownEvent.ts b/packages/vue/src/grid/src/keyboard/src/utils/triggerCellMousedownEvent.ts index 77a4b71787..29b1451de8 100644 --- a/packages/vue/src/grid/src/keyboard/src/utils/triggerCellMousedownEvent.ts +++ b/packages/vue/src/grid/src/keyboard/src/utils/triggerCellMousedownEvent.ts @@ -1,30 +1,6 @@ -/** - * 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 { throttle } from '@opentiny/utils' import { hasClass } from '@opentiny/utils' -import { getRowNodes, getCellNodeIndex, getEventTargetNode } from '@opentiny/vue-renderless/grid/utils' +import { getRowNodes, getCellNodeIndex, getEventTargetNode } from '../../../utils/utils' export function onCellMousedownGridEl(args) { let { diff --git a/packages/vue/src/grid/src/keyboard/src/utils/triggerHeaderCellMousedownEvent.ts b/packages/vue/src/grid/src/keyboard/src/utils/triggerHeaderCellMousedownEvent.ts index 2037541a98..9a65a064fe 100644 --- a/packages/vue/src/grid/src/keyboard/src/utils/triggerHeaderCellMousedownEvent.ts +++ b/packages/vue/src/grid/src/keyboard/src/utils/triggerHeaderCellMousedownEvent.ts @@ -1,30 +1,6 @@ -/** - * 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 { addClass, removeClass, hasClass } from '@opentiny/utils' import { throttle } from '@opentiny/utils' -import { getRowNodes, getCellNodeIndex, getEventTargetNode } from '@opentiny/vue-renderless/grid/utils' +import { getRowNodes, getCellNodeIndex, getEventTargetNode } from '../../../utils/utils' export function handleHeaderCellMousedownEvent({ $el, _vm, bodyList, cell, headerList, startCell }) { let oldMousemove = document.onmousemove diff --git a/packages/vue/src/grid/src/loading/index.ts b/packages/vue/src/grid/src/loading/index.ts index 8e0be293f4..6add4d4fba 100644 --- a/packages/vue/src/grid/src/loading/index.ts +++ b/packages/vue/src/grid/src/loading/index.ts @@ -10,7 +10,7 @@ * */ import { $install } from '@opentiny/vue-common' -import Loading from './src/loading' +import Loading from './src/loading.vue' $install(Loading) diff --git a/packages/vue/src/grid/src/loading/src/loading.vue b/packages/vue/src/grid/src/loading/src/loading.vue new file mode 100644 index 0000000000..2275d14cee --- /dev/null +++ b/packages/vue/src/grid/src/loading/src/loading.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/vue/src/grid/src/menu/src/methods.ts b/packages/vue/src/grid/src/menu/src/methods.ts index 661b76266b..3357299ff4 100644 --- a/packages/vue/src/grid/src/menu/src/methods.ts +++ b/packages/vue/src/grid/src/menu/src/methods.ts @@ -22,11 +22,11 @@ * SOFTWARE. * */ -import { findIndexOf } from '@opentiny/vue-renderless/grid/static/' -import { hasChildrenList, getDomNode, emitEvent } from '@opentiny/vue-renderless/grid/utils' +import { findIndexOf } from '../../utils/static/' +import { hasChildrenList, getDomNode, emitEvent } from '../../utils/utils' import { Menus } from '../../adapter' -let getSelectItem = (subList) => { +const getSelectItem = (subList) => { for (let i = 0; i < subList.length; i++) { let menu = subList[i] @@ -38,7 +38,7 @@ let getSelectItem = (subList) => { export default { // 关闭快捷菜单 - _closeMenu() { + closeMenu() { Object.assign(this.ctxMenuStore, { visible: false, selected: null, diff --git a/packages/vue/src/grid/src/menu/src/panel.ts b/packages/vue/src/grid/src/menu/src/panel.ts index 84481cc41a..0ea1f14f8f 100644 --- a/packages/vue/src/grid/src/menu/src/panel.ts +++ b/packages/vue/src/grid/src/menu/src/panel.ts @@ -22,7 +22,7 @@ * SOFTWARE. * */ -import { getFuncText } from '@opentiny/vue-renderless/grid/utils' +import { getFuncText } from '../../utils/utils' import { h, $prefix, defineComponent } from '@opentiny/vue-common' import Children from './children' 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 @@ diff --git a/packages/vue/src/grid/src/table/src/events.ts b/packages/vue/src/grid/src/table/src/events.ts index 77e5581874..c647076901 100644 --- a/packages/vue/src/grid/src/table/src/events.ts +++ b/packages/vue/src/grid/src/table/src/events.ts @@ -1,27 +1,9 @@ /** - * 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 { handleGlobalMousedownOnFilterWrapper, handleGlobalMousedownOnCtxMenu, @@ -29,12 +11,27 @@ import { handleGlobalIsClear, handleGlobalClearActived } from './utils/handleGlobalMousedownEvent' -import { findTree } from '@opentiny/vue-renderless/grid/static/' -import { setCellValue, hasChildrenList } from '@opentiny/vue-renderless/grid/utils' + +// 导入树形数据处理工具 +import { findTree } from '../../utils/static/' + +// 导入单元格值设置和子节点检查工具 +import { setCellValue, hasChildrenList } from '../../utils/utils' + +// 导入其他按键检查工具 import { checkOtherKey } from './utils/handleOtherKeyDown' + +// 导入全局键盘事件处理工具 import { onGlobalKeydown } from './utils/handleGlobalKeydownEvent' -// 全局按下事件处理 +/** + * 处理全局鼠标按下事件 + * @param {Event} event - 鼠标事件对象 + * @description + * 1. 处理过滤面板的点击事件 + * 2. 处理编辑状态的清除 + * 3. 处理快捷菜单的关闭 + */ export function handleGlobalMousedownEvent(event) { let { $el, ctxMenuStore, editConfig = {}, filterStore } = this let { filterWrapper, validTip } = this.$refs @@ -62,23 +59,49 @@ export function handleGlobalMousedownEvent(event) { handleGlobalMousedownOnCtxMenu({ _vm: this, ctxMenuStore, event }) } +/** + * 处理全局鼠标按下捕获事件 + * @param {Event} event - 鼠标事件对象 + * @description 清除鼠标选中状态 + */ export function handleGlobalMousedownCaptureEvent(event) { this.handleClearMouseChecked(event) } -// 窗口失焦事件处理 +/** + * 处理全局失焦事件 + * @description 关闭过滤面板和菜单 + */ export function handleGlobalBlurEvent() { this.closeFilter() this.closeMenu() } -// 全局滚动事件 +/** + * 处理全局鼠标滚轮事件 + * @param {Event} event - 鼠标滚轮事件对象 + * @description + * 1. 更新滚动加载条 + * 2. 关闭提示框 + * 3. 关闭菜单 + */ export function handleGlobalMousewheelEvent(event) { this.updateScrollLoadBar(event) this.clostTooltip() this.closeMenu() } +/** + * 处理ESC键按下事件 + * @param {Object} params - 参数对象 + * @param {Event} params.event - 键盘事件对象 + * @param {Object} params.actived - 当前激活的单元格信息 + * @param {Object} params.mouseConfig - 鼠标配置信息 + * @description + * 1. 关闭菜单和过滤面板 + * 2. 如果处于编辑状态,则取消编辑 + * 3. 如果配置了选中功能,则设置为选中状态 + */ export function handleEscKeyDown({ event, actived, mouseConfig }) { this.closeMenu() this.closeFilter() @@ -92,6 +115,16 @@ export function handleEscKeyDown({ event, actived, mouseConfig }) { } } +/** + * 处理回车键按下事件 + * @param {Object} params - 参数对象 + * @param {Event} params.event - 键盘事件对象 + * @param {Object} params.selected - 当前选中的单元格信息 + * @param {Object} params.actived - 当前激活的单元格信息 + * @description + * 1. 如果处于选中或激活状态,移动到下一行 + * 2. 如果是树形表格且当前行有子节点,展开并移动到第一个子节点 + */ export function handleEnterKeyDown({ event, selected, actived }) { const { highlightCurrentRow, currentRow, treeConfig } = this const isLeftArrow = event.keyCode === 37 @@ -120,6 +153,14 @@ export function handleEnterKeyDown({ event, selected, actived }) { } } +/** + * 处理右键菜单事件 + * @param {Object} params - 参数对象 + * @param {Event} params.event - 鼠标事件对象 + * @description + * 1. 如果当前菜单项有子菜单,处理子菜单的移动 + * 2. 否则处理主菜单的移动 + */ export function handleCtxMenu({ event }) { const { ctxMenuStore } = this event.preventDefault() @@ -146,6 +187,15 @@ export function handleCtxMenu({ event }) { } } +/** + * 处理方向键按下事件 + * @param {Object} params - 参数对象 + * @param {Event} params.event - 键盘事件对象 + * @param {Object} params.selected - 当前选中的单元格信息 + * @description + * 1. 如果选中了单元格,则移动选中位置 + * 2. 如果启用了当前行高亮,则上下移动当前行 + */ export function handleArrowKeyDown({ event, selected }) { const { highlightCurrentRow, currentRow } = this let isLeftArrow = event.keyCode === 37 @@ -160,6 +210,15 @@ export function handleArrowKeyDown({ event, selected }) { } } +/** + * 处理删除键按下事件 + * @param {Object} params - 参数对象 + * @param {Event} params.event - 键盘事件对象 + * @param {Object} params.selected - 当前选中的单元格信息 + * @description + * 1. 如果启用了删除功能,清空选中单元格的值 + * 2. 如果是树形表格且按下了退格键,关闭当前节点并返回父节点 + */ export function handleDelKeyDown({ event, selected }) { const { keyboardConfig = {}, treeConfig, highlightCurrentRow, currentRow } = this const isBack = event.keyCode === 8 @@ -180,6 +239,15 @@ export function handleDelKeyDown({ event, selected }) { } } +/** + * 处理空格键按下事件 + * @param {Object} params - 参数对象 + * @param {Event} params.event - 键盘事件对象 + * @param {Object} params.selected - 当前选中的单元格信息 + * @description + * 1. 如果是选择列,切换行的选中状态 + * 2. 否则触发单选行事件 + */ export function handleSpaceKeyDown({ event, selected }) { event.preventDefault() if (selected.column.type === 'selection') { @@ -189,6 +257,14 @@ export function handleSpaceKeyDown({ event, selected }) { } } +/** + * 处理Tab键按下事件 + * @param {Object} params - 参数对象 + * @param {Event} params.event - 键盘事件对象 + * @param {Object} params.selected - 当前选中的单元格信息 + * @param {Object} params.actived - 当前激活的单元格信息 + * @description 根据是否按下Shift键,在选中或激活的单元格之间移动 + */ export function handleTabKeyDown({ event, selected, actived }) { const isShiftKey = event.shiftKey const useTab = this.editConfig.useTab @@ -200,6 +276,15 @@ export function handleTabKeyDown({ event, selected, actived }) { } } +/** + * 处理复制相关按键事件 + * @param {Object} params - 参数对象 + * @param {Event} params.event - 键盘事件对象 + * @description + * 1. Ctrl+A: 全选 + * 2. Ctrl+X/C: 剪切/复制 + * 3. Ctrl+V: 粘贴 + */ export function handleCopyKeyDown({ event }) { if (event.keyCode === 65) { this.handleAllChecked(event) @@ -211,6 +296,13 @@ export function handleCopyKeyDown({ event }) { } } +/** + * 处理F2键按下事件 + * @param {Object} params - 参数对象 + * @param {Event} params.event - 键盘事件对象 + * @param {Object} params.selected - 当前选中的单元格信息 + * @description 如果选中了单元格,则激活编辑状态 + */ export function handleF2KeyDown({ event, selected }) { if (selected.row && selected.column) { event.preventDefault() @@ -218,6 +310,13 @@ export function handleF2KeyDown({ event, selected }) { } } +/** + * 处理其他按键事件 + * @param {Object} params - 参数对象 + * @param {Event} params.event - 键盘事件对象 + * @param {Object} params.selected - 当前选中的单元格信息 + * @description 如果按下的是非功能键且单元格可编辑,则激活编辑状态 + */ export function handleOtherKeyDown({ event, selected }) { const { keyboardConfig = {} } = this const keyCode = event.keyCode @@ -234,16 +333,24 @@ export function handleOtherKeyDown({ event, selected }) { } } -// 全局键盘事件 +/** + * 处理全局键盘事件 + * @param {Event} event - 键盘事件对象 + * @description 调用全局键盘事件处理函数 + */ export function handleGlobalKeydownEvent(event) { this.preventEvent(event, 'event.keydown', { $table: this }, () => { onGlobalKeydown(event, this) }) } -// 监听全局的窗口尺寸改变事件,然后重新计算表格样式 +/** + * 处理全局窗口大小改变事件 + * @description + * 1. 更新父容器高度 + * 2. 重新计算表格样式 + */ export function handleGlobalResizeEvent() { // 窗口resize后,调用recalculate父容器高度还是初始值,需要update一下 this.updateParentHeight() - this.recalculate() } diff --git a/packages/vue/src/grid/src/table/src/funcs.ts b/packages/vue/src/grid/src/table/src/funcs.ts deleted file mode 100644 index 2a681117f6..0000000000 --- a/packages/vue/src/grid/src/table/src/funcs.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * 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 { warn } from '../../tools' -import { isArray, get } from '@opentiny/vue-renderless/grid/static/' -import { preprocessDataObjectFormat, preventDupRender, handleResolveColumnComplete } from './utils/handleResolveColumn' - -// Module methods -export const funcs = [ - 'closeMenu', - 'getMouseSelecteds', - 'getMouseCheckeds', - 'clearCopyed', - 'clearChecked', - 'clearHeaderChecked', - 'clearIndexChecked', - 'clearSelected', - 'insert', - 'insertAt', - 'remove', - 'removeSelecteds', - 'revert', - 'revertData', - 'getRecordset', - 'getInsertRecords', - 'getRemoveRecords', - 'getUpdateRecords', - 'clearActived', - 'getActiveRow', - 'hasActiveRow', - 'setActiveRow', - 'setActiveCell', - 'setSelectCell', - 'clearValidate', - 'fullValidate', - 'validate', - 'exportCsv', - 'exportExcel' -] - -// 分组表头的属性 -export const headerProps = { - children: 'children' -} - -export const handleAllColumnPromises = (opt, ctx) => { - let { startIndex, fetchColumns, tableData, asyncRenderMap, isScrollLoad } = opt - return (data) => { - if (data.length) { - // 【data属性设置表格数据且开启可视区】防止 渲染导致滚动条跳动(跳到初始位置) - ctx._isUpdateData = true - data.forEach((item, i) => { - let columnValues = [] - let columnValuesMap = {} - let k = startIndex // 查找起始位置 - let renderCount = 0 - let columnCount = 0 - const columnData = [] - const { format = {}, property } = fetchColumns[i] - const { splitConfig = {}, fields = {}, complete } = format.async || {} - - columnValues = isArray(item) ? item : get(item, fields.data || 'values') - columnCount = columnValues.length - // 预处理数据对象格式 - preprocessDataObjectFormat({ columnCount, columnValues, columnValuesMap, fields }) - for (let len = tableData.length; k < len; k++) { - const row = tableData[k] - const cellTexts = [] - const uniqueKey = ctx.getAsyncColumnUniqueKey(property, row) - const cellValuesCount = asyncRenderMap[uniqueKey] - const asyncColumnName = ctx.getAsyncColumnName(property) - const isRender = !!row[asyncColumnName] - let args = { asyncColumnName, cellTexts, cellValuesCount, columnData } - Object.assign(args, { columnValuesMap, isRender, property, renderCount, row, splitConfig }) - // 防止重复渲染 - renderCount = preventDupRender(args) - // 针对可视区滚动优化 - if (isScrollLoad && renderCount >= columnCount) { - break - } - } - format.data = columnData - // 用户自定义缓存机制的接口 - handleResolveColumnComplete({ _vm: ctx, columnData, complete }) - }) - ctx.tableData = ctx.tableData.slice(0) - ctx.$nextTick(() => { - ctx._isUpdateData = false - }) - } else { - warn('Unknown error:the query data is empty.') - } - } -} diff --git a/packages/vue/src/grid/src/table/src/methods.ts b/packages/vue/src/grid/src/table/src/methods.ts index b0363fde91..ddd9ef639b 100644 --- a/packages/vue/src/grid/src/table/src/methods.ts +++ b/packages/vue/src/grid/src/table/src/methods.ts @@ -1,38 +1,7 @@ -/* eslint-disable unused-imports/no-unused-vars */ -/** - * 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 { getColumnList, assemColumn } from '@opentiny/vue-renderless/grid/utils' -import { toDecimal } from '@opentiny/utils' -import { addClass, removeClass, isDisplayNone } from '@opentiny/utils' -import { isNull } from '@opentiny/utils' -import { debounce } from '@opentiny/utils' -import { fastdom } from '@opentiny/utils' +import { getColumnList, assemColumn } from '../../utils/utils' +import { addClass, removeClass, isDisplayNone, isNull, toDecimal } from '@opentiny/utils' +import { fastdom, isNumber } from '@opentiny/utils' import { - isNumber, - filterTree, - isArray, isBoolean, findTree, set, @@ -46,11 +15,9 @@ import { isEqual, mapTree, clone, - destructuring, - clear, sum, find -} from '@opentiny/vue-renderless/grid/static/' +} from '../../utils/static/' import { isPx, isScale, @@ -61,11 +28,10 @@ import { setCellValue, getRowid, emitEvent -} from '@opentiny/vue-renderless/grid/utils' +} from '../../utils/utils' import Cell from '../../cell' import { error, warn } from '../../tools' import TINYGrid, { Interceptor } from '../../adapter' -import GlobalConfig from '../../config' import { handleLayout } from './utils/updateStyle' import { isTargetRadioOrCheckbox, @@ -84,12 +50,8 @@ import { showGroupFixedError, onScrollXLoad } from './utils/refreshColumn' -import { mapFetchColumnPromise } from './utils/handleResolveColumn' -import { hooks, isVue2 } from '@opentiny/vue-common' -import { computeScrollYLoad, computeScrollXLoad } from './utils/computeScrollLoad' import { calcTableWidth, calcFixedStickyPosition } from './utils/autoCellWidth' import { generateFixedClassName } from './utils/handleFixedColumn' -import { funcs, headerProps, handleAllColumnPromises } from './funcs' import { handleGlobalMousedownEvent, handleGlobalBlurEvent, @@ -112,94 +74,34 @@ import { import { getRowUniqueId, getTableRowKey, - getTotalRows, setTreeScrollYCache, sliceFullData, - sliceVisibleColumn, setSliceColumnTree, buildRowGroupFullData } from './strategy' +import { updateRowStatus, getCellStatus } from '../../composable' +import { buildCache } from './utils/handleCacheData' -let run = (names, $table) => names.forEach((name) => $table[name].apply($table)) -let debounceScrollLoadDuration = 200 -let AsyncCollectTimeout = 100 - -// 多字段排序 -const sortMultiple = (rows, columns, _vm) => { - const greaterThan = (valueP, valueQ) => { - const typeP = typeof valueP - const typeQ = typeof valueQ - if (typeP === typeQ && ['number', 'string', 'boolean'].includes(typeP)) { - return valueP > valueQ - } else { - return String(valueP) > String(valueQ) - } - } - - const { multipleColumnSort } = _vm.sortOpts - - if (typeof multipleColumnSort === 'function') { - rows = multipleColumnSort({ $table: _vm, tableData: rows, sortColumns: columns }) - } else { - rows = rows.sort((p, q) => { - for (let i = 0; i < columns.length; i++) { - const { property, order } = columns[i] - const flag = order === 'asc' ? 1 : -1 - const valueP = p[property] - const valueQ = q[property] - - if (!Object.is(valueP, valueQ)) { - return greaterThan(valueP, valueQ) ? flag : -flag - } - } - - return 0 - }) - } - - return rows -} - -// 创建快速缓存 -const buildCache = (tableData, { treeConfig, treeOrdered }) => { - const backupMap = new WeakMap() - const { children, temporaryIndex = '_$index_' } = treeConfig || {} - const isTreeOrderedFalse = treeConfig && !treeOrdered - - const traverse = (arr, rowLevel, parentIndex) => { - const backup = [] - - if (Array.isArray(arr) && arr.length > 0) { - arr.forEach((row, rowIndex) => { - if (isTreeOrderedFalse) { - row[temporaryIndex] = `${parentIndex ? `${parentIndex}.` : ''}${rowIndex + 1}` - } - - // 深拷贝 - const backupRow = clone({ ...row, [children]: null }, true) - - backup.push(backupRow) - backupMap.set(row, backupRow) - - if (row[children]) { - backupRow[children] = traverse(row[children], rowLevel + 1, isTreeOrderedFalse ? row[temporaryIndex] : '') - } - }) - } - - return backup - } - - const backupData = traverse(tableData, 0, '') - - return { backupData, backupMap } +// 分组表头的属性 +const headerProps = { + children: 'children' } const Methods = { + /** + * 获取父级元素 + * @returns {HTMLElement} 父级DOM元素 + */ getParentElem() { let $el = this.$grid ? this.$grid.$el : this.$el return $el.parentNode }, + + /** + * 更新父级元素高度 + * 如果存在$grid则调用其updateParentHeight方法 + * 否则直接获取父元素高度 + */ updateParentHeight() { if (this.$grid) { this.$grid.updateParentHeight() @@ -207,40 +109,55 @@ const Methods = { this.parentHeight = this.getParentElem().clientHeight } }, + + /** + * 获取父级元素高度 + * @returns {number} 父级元素高度 + */ getParentHeight() { return this.parentHeight }, + + /** + * 清除所有状态 + * @param {boolean} silent - 是否静默清除(不触发事件) + * @returns {Promise} 清除完成后的Promise + */ clearAll(silent) { const { fetchOption = {} } = this.$grid const { isReloadFilter, isReloadScroll = false } = fetchOption - let functionNames = [ - 'clearScroll', - 'clearSort', - 'clearCurrentRow', - 'clearCurrentColumn', - 'clearSelection', - 'clearRowExpand', - 'clearTreeExpand' - ] - - // 存在配置时,移除 clearScroll, 重载数据时不清除滚动位置 - if (isReloadScroll) { - functionNames = functionNames.filter((i) => i !== 'clearScroll') - } - - run(functionNames, this) + this.clearActived() + this.clearCurrentRow() + this.clearCurrentColumn() + this.clearRadioRow() + this.clearRowExpand() + !isReloadScroll && this.clearScroll() + this.clearSort() + this.cellStatus.clear() + // 根据配置决定是否重新加载过滤 if (typeof isReloadFilter === 'undefined' ? TINYGrid._filter : !isReloadFilter) { this.clearFilter(silent) } + // 清除选中状态 if (this.keyboardConfig || this.mouseConfig) { - run(['clearIndexChecked', 'clearHeaderChecked', 'clearChecked', 'clearSelected', 'clearCopyed'], this) + this.clearIndexChecked() + this.clearHeaderChecked() + this.clearChecked() + this.clearSelected() + this.clearCopyed() } return this.clearActived() }, + + /** + * 刷新表格数据 + * @param {Array} data - 新的表格数据 + * @returns {Promise} 刷新完成后的Promise + */ refreshData(data) { const next = () => { this.tableData = [] @@ -248,30 +165,23 @@ const Methods = { } return this.$nextTick().then(next) }, - refreshStyle() { - let { $el, rowSpan, spanMethod } = this - // 存在合并时才刷新样式 - if ($el && (rowSpan || spanMethod)) { - let transform = $el.style.transform - let restore = () => - setTimeout(() => { - $el.style.transform = transform - }) - $el.style.transform = 'scale(0.99999)' - return this.$nextTick().then(restore) - } - return this.$nextTick() - }, + + /** + * 更新表格数据 + * @returns {Promise} 更新完成后的Promise + */ updateData() { - return this.handleTableData(true) - .then(() => this.updateFooter()) - .then(() => this.recalculate()) + return this.handleTableData(true).then(() => this.updateFooter()) }, - // 处理表格数据(过滤,排序,虚拟滚动需要渲染数据的条数) + + /** + * 处理表格数据 + * @param {boolean} force - 是否强制更新 + * @returns {Promise} 处理完成后的Promise + */ handleTableData(force) { // 这里处理是否强制刷新afterFullData:经过筛选排序后的数据 force && this.updateAfterFullData() - // 判断是否有虚拟滚动,有即剪切数据 this.tableData = sliceFullData(this) @@ -280,17 +190,32 @@ const Methods = { return this.$nextTick() }, - // 全量加载表格数据 + + /** + * 加载表格数据 + * @param {Array} datas - 要加载的数据 + * @param {boolean} notRefresh - 是否不刷新表格状态 + * @returns {Promise} 加载完成后的Promise + */ loadTableData(datas, notRefresh) { - let { $grid, $refs, editStore, height, maxHeight, treeConfig, lastScrollLeft, lastScrollTop, optimizeOpts } = - this as any - let { fetchOption = {} } = $grid - let { isReloadScroll = false } = fetchOption - let { scrollY } = optimizeOpts + const { + $grid, + editStore, + height, + maxHeight, + lastScrollLeft, + lastScrollTop, + optimizeOpts, + treeConfig, + treeOrdered + } = this as any + const { fetchOption = {} } = $grid + const { isReloadScroll = false } = fetchOption + const { scrollY } = optimizeOpts // 浅拷贝原始全量数据 - let tableFullData = isArray(datas) ? datas.slice(0) : [] + const tableFullData = Array.isArray(datas) ? datas.slice(0) : [] // 是否开启纵向的虚拟滚动,默认大于等于500条开启纵向的虚拟滚动 - let scrollYLoad = scrollY && scrollY.gt > 0 && scrollY.gt <= tableFullData.length + const scrollYLoad = scrollY && scrollY.gt > 0 && scrollY.gt <= tableFullData.length // 初始化新增和删除数据列表 editStore.insertList = [] @@ -300,7 +225,7 @@ const Methods = { // 缓存数据 this.updateCache(true) // 深拷贝原始数据,并建立表格行数据和行数据对应的原始数据的映射关系,可以极大的提高检查编辑态单元格status和还原行数据的速度 - const { backupData, backupMap } = buildCache(tableFullData, this) + const { backupData, backupMap } = buildCache(tableFullData, { treeConfig, treeOrdered }) // tableSynchData:用户传递拖来的原始数据,tableSourceData:深拷贝用户传递过来的初始原始数据 Object.assign(this, { tableSynchData: datas, tableSourceData: backupData, backupMap, scrollYLoad }) @@ -315,84 +240,93 @@ const Methods = { // 对全量数据进行筛选、排序、虚拟滚动切割数据等一系列操作 this.handleTableData(true) - // reserveCheckSelection:处理分页切换保留选中状态的逻辑,checkSelectionStatus:处理全选、半选等选中状态 - run(['reserveCheckSelection', 'checkSelectionStatus'], this) - let first = () => !notRefresh && this.recalculate() - let second = () => { + + // reserveCheckSelection:处理分页切换保留选中状态的逻辑 + // checkSelectionStatus:处理全选、半选等选中状态 + this.reserveCheckSelection() + this.checkSelectionStatus() + + // 定义第二个处理函数:尝试恢复滚动位置 + const second = () => { // 让表格滚动条滚动到最后一次滚动到的位置 if (lastScrollLeft || lastScrollTop) { return this.attemptRestoreScoll({ lastScrollLeft, lastScrollTop }) - } else { - // 重置表头滚动条位置 - let headerElem = $refs.tableHeader ? $refs.tableHeader.$el : null - headerElem && (headerElem.scrollLeft = 0) } } - return this.$nextTick().then(first).then(second) + + // 链式执行两个处理函数 + return this.$nextTick().then(second) }, - // 重新加载数据 + + /** + * 重新加载数据 + * 先清空所有状态,然后加载新数据,最后处理默认设置 + * @param {Array} datas - 要加载的数据 + * @returns {Promise} 加载完成后的Promise + */ reloadData(datas) { return this.clearAll() .then(() => this.loadTableData(datas)) .then(() => this.handleDefault()) }, - // 加载全量数据 + + /** + * 加载数据 + * @param {Array} datas - 要加载的数据 + * @returns {Promise} 加载完成后的Promise + */ loadData(datas) { return new Promise((resolve) => { this.loadTableData(datas) resolve() }) }, - reloadRow(row, record, field) { - let { tableData, tableSourceData } = this - let rowIndex = this.getRowIndex(row) - let originRow = tableSourceData[rowIndex] - let hasSrc = originRow && row - let hasSrcNoField = hasSrc && !field - if (hasSrc && field) { - set(originRow, field, get(record || row, field)) - } - if (hasSrcNoField && record) { - tableSourceData[rowIndex] = record - clear(row, undefined) - Object.assign(row, this.defineField({ ...record })) - this.updateCache(true) - } - if (hasSrcNoField && !record) { - destructuring(originRow, clone(row, true)) - } - this.tableData = tableData.slice(0) - return this.$nextTick() - }, - // 从新加载列配置 - reloadColumn(columns) { - return this.clearAll().then(() => this.loadColumn(columns)) - }, + + /** + * 加载列配置 + * @param {Array} columns - 要加载的列配置 + * @returns {Promise} 加载完成后的Promise + */ loadColumn(columns) { return new Promise((resolve) => { + // 通过mapTree函数处理每个列配置,创建列实例 this.collectColumn = mapTree(columns, (column) => Cell.createColumn(this, column), headerProps) resolve() }).then(() => this.$nextTick()) }, - // 更新数据的 Map + + /** + * 更新数据的映射缓存 + * @param {boolean} source - 是否更新源数据 + */ updateCache(source) { let { fullAllDataRowIdData, fullAllDataRowMap, fullDataRowIdData, fullDataRowMap, tableFullData, treeConfig } = this let rowKey = getTableRowKey(this) + + // 构建行数据缓存的函数 let buildRowCache = (row, index) => { + // 获取行ID,如果没有则生成 let rowId = getRowid(this, row) if (isNull(rowId) || rowId === '') { rowId = getRowUniqueId() set(row, rowKey, rowId) } + + // 创建行缓存对象 let rowCache = { row, rowid: rowId, index } + + // 根据source参数决定是否更新完整数据缓存 if (source) { fullDataRowIdData[rowId] = rowCache fullDataRowMap.set(row, rowCache) } + + // 更新全部数据缓存 fullAllDataRowIdData[rowId] = rowCache fullAllDataRowMap.set(row, rowCache) } + // 清空缓存的函数 let clearCache = () => { fullAllDataRowIdData = {} this.fullAllDataRowIdData = fullAllDataRowIdData @@ -403,7 +337,11 @@ const Methods = { fullDataRowMap.clear() } } + + // 执行清空缓存 clearCache() + + // 根据数据是否为树结构,使用不同方式遍历数据并构建缓存 if (treeConfig) { eachTree(tableFullData, buildRowCache, treeConfig) } else { @@ -412,30 +350,40 @@ const Methods = { }, // 更新列的 Map cacheColumnMap() { - let { fullColumnMap, tableFullColumn: fullColumn } = this - let fullColumnIdData = {} - this.fullColumnIdData = fullColumnIdData - Map.prototype.clear.apply(fullColumnMap) - fullColumn.forEach((column, index) => { - let colCache = { colid: column.id, column, index } + const { fullColumnMap, tableFullColumn } = this + const fullColumnIdData = {} + + // 清空旧的列映射 + fullColumnMap.clear() + + // 为每列创建缓存对象并同时更新映射 + tableFullColumn.forEach((column, index) => { + const colCache = { colid: column.id, column, index } fullColumnIdData[column.id] = colCache fullColumnMap.set(column, colCache) }) + + // 更新实例的列ID数据 + this.fullColumnIdData = fullColumnIdData }, - // 通过tr的dom元素获取行数据等相关信息 + // 通过tr的DOM元素获取对应的行数据及相关信息 getRowNode(tr) { if (!tr) { return null } const { fullAllDataRowIdData, tableFullData, treeConfig } = this + // 获取行DOM元素上存储的行ID属性 const dataRowid = tr.getAttribute('data-rowid') + + // 如果是树形结构,使用findTree查找对应的行数据 if (treeConfig) { const matches = findTree(tableFullData, (row) => getRowid(this, row) === dataRowid, treeConfig) if (matches) { return matches } } else { + // 如果是普通表格,直接从缓存中获取 if (fullAllDataRowIdData[dataRowid]) { const rowCache = fullAllDataRowIdData[dataRowid] return { @@ -447,19 +395,25 @@ const Methods = { } return null }, + + // 通过单元格DOM元素获取对应的列信息 getColumnNode(cell) { if (!cell) { return null } const { isGroup, fullColumnIdData, tableFullColumn } = this + // 获取单元格DOM元素上存储的列ID属性 const dataColid = cell.getAttribute('data-colid') const colCache = fullColumnIdData?.[dataColid] + + // 如果是分组表头,使用findTree查找对应的列 if (isGroup) { let matches = findTree(tableFullColumn, (column) => column.id === dataColid, headerProps) if (matches) { return matches } } else if (colCache) { + // 如果是普通表头,直接从缓存中获取 return { index: colCache.index, item: colCache.column, @@ -468,27 +422,37 @@ const Methods = { } return null }, + + // 获取行在数据数组中的索引 getRowIndex(row) { let { fullDataRowMap } = this return fullDataRowMap.has(row) ? fullDataRowMap.get(row).index : -1 }, + + // 获取列在列数组中的索引 getColumnIndex(column) { let { fullColumnMap } = this return fullColumnMap.has(column) ? fullColumnMap.get(column).index : -1 }, + + // 判断列是否为序号列 hasIndexColumn(column) { return column && column.type === 'index' }, + + // 定义行数据字段,确保行数据具有所有必要字段 defineField(row, copy) { if (!row || typeof row !== 'object') { return row } + // 如果需要复制,则创建行数据的深拷贝 if (copy) { row = clone(row, true) } let { visibleColumn } = this let rowKey = getTableRowKey(this) + // 为每个可见列,如果行中没有对应的属性,则设置默认值 visibleColumn.forEach(({ property, editor }) => { let propNotExist = property && !has(row, property) let propDefaultValue = editor && !isUndefined(editor.defaultValue) ? editor.defaultValue : null @@ -496,27 +460,35 @@ const Methods = { set(row, property, propDefaultValue) } }) - // 如果行数据的唯一主键不存在,则生成 + + // 如果行数据的唯一主键不存在,则生成新的唯一ID const rowId = get(row, rowKey) if (isNull(rowId) || rowId === '') { set(row, rowKey, getRowUniqueId()) } return row }, + + // 判断行是否为临时行(如新增的未保存行) isTemporaryRow(row) { let rowid = getRowid(this, row) return find(this.temporaryRows, (r) => rowid === getRowid(this, r)) }, + + // 创建新的数据记录 createData(records, copy) { - let isArr = isArray(records) + let isArr = Array.isArray(records) if (!isArr) { records = [records] } + // 对每条记录应用defineField处理 let tmp = records.map((record) => this.defineField(record, copy)) return new Promise((resolve) => { resolve(isArr ? tmp : tmp[0]) }) }, + + // 创建新行(会创建副本) createRow(records) { return this.createData(records, true) }, @@ -529,21 +501,37 @@ const Methods = { * 如果还额外传了field,则清空指定单元格内容; */ clearData(rows, field) { - rows = !arguments.length ? this.tableFullData : rows && !isArray(rows) ? [rows] : rows - rows.forEach((row) => { + // 获取需要清空的行数据 + const targetRows = !rows + ? this.tableFullData // 不传参数时清空所有行 + : Array.isArray(rows) + ? rows // 传入数组时直接使用 + : rows + ? [rows] + : [] // 传入单行时转为数组,否则使用空数组 + + // 清空数据 + targetRows.forEach((row) => { if (field) { + // 清空指定字段 set(row, field, null) } else { - this.visibleColumn.forEach((column) => { - column.property && setCellValue(row, column, null) - }) + // 清空所有可见列的字段值 + this.visibleColumn + .filter((col) => col.property) // 过滤有property属性的列 + .forEach((col) => setCellValue(row, col, null)) } }) + return this.$nextTick() }, + + // 判断行是否为插入的新行 hasRowInsert(row) { - return ~this.editStore.insertList.indexOf(row) + return this.editStore.insertList.includes(row) }, + + // 比较行数据的指定字段值是否相等 compareRow(row, originalRow, field) { const value = get(row, field) const originalValue = get(originalRow, field) @@ -563,16 +551,21 @@ const Methods = { return result }, + + // 判断行数据是否有变更 hasRowChange(row, field) { const { tableSourceData, treeConfig, visibleColumn, backupMap, editConfig } = this const insertChanged = editConfig?.insertChanged ?? false const argsLength = arguments.length const rowId = getRowid(this, row) let originRow - // 新增的数据不需要检测 + + // 新增的数据不需要检测变更,直接根据配置返回 if (this.isTemporaryRow(row)) { return insertChanged } + + // 处理树形结构的行比较 if (treeConfig) { const children = treeConfig.children const cacheRow = backupMap.get(row) @@ -583,13 +576,17 @@ const Methods = { originRow = { ...cacheRow, [children]: null } } } else { + // 获取原始行数据 originRow = find(tableSourceData, (item) => rowId === getRowid(this, item)) } + if (originRow) { + // 如果指定了字段,只比较该字段 if (argsLength > 1) { return !this.compareRow(row, originRow, field) } + // 否则比较所有可见列的字段 for (let i = 0; i < visibleColumn.length; i++) { let { property } = visibleColumn[i] if (property && !this.compareRow(row, originRow, property)) { @@ -599,22 +596,28 @@ const Methods = { } return false }, + // 获取表格所有列 getColumns(columnIndex) { let { visibleColumn: columns } = this let argsLength = arguments.length return argsLength ? columns[columnIndex] : columns.slice(0) }, + + // 根据列ID获取列对象 getColumnById(colid) { let { fullColumnIdData } = this let colCache = fullColumnIdData[colid] return colCache ? colCache.column : null }, + + // 根据字段名获取列对象 getColumnByField(field) { let { visibleColumn: columns } = this return typeof field === 'string' && field ? find(columns, (column) => column.property === field) : null }, - // 获取当前表格的列(完整的全量表头列、处理条件之后的全量表头列、当前渲染中的表头列) + + // 获取当前表格的列配置(完整的全量表头列、处理条件之后的全量表头列、当前渲染中的表头列) getTableColumn() { let { collectColumn, tableColumn, tableFullColumn, visibleColumn } = this return { @@ -624,6 +627,7 @@ const Methods = { collectColumn: collectColumn.slice(0) } }, + // 获取表格所有数据 getData(rowIndex) { let tableSynchData = this.data || this.tableSynchData @@ -638,26 +642,6 @@ const Methods = { } return undefined }, - // 获取选中数据。notCopy为true不返回数据副本,表格内部要继续处理其返回值时设置为true - getSelectRecords(notCopy) { - let { selectConfig = {}, selection } = this - let { tableFullData, treeConfig } = this - let { checkField } = selectConfig - let { rowList = [] } = {} - if (checkField && treeConfig) { - rowList = filterTree(tableFullData, (row) => get(row, checkField), treeConfig) - } - if (checkField && !treeConfig) { - rowList = tableFullData.filter((row) => get(row, checkField)) - } - if (!checkField && treeConfig) { - rowList = filterTree(tableFullData, (row) => ~selection.indexOf(row), treeConfig) - } - if (!checkField && !treeConfig) { - rowList = tableFullData.filter((row) => ~selection.indexOf(row)) - } - return notCopy ? rowList : clone(rowList, true) - }, // 对数据进行筛选和排序,获取处理后数据。服务端筛选和排序,在接口调用时已传入参数 updateAfterFullData() { let { remoteFilter, remoteSort, tableFullData, visibleColumn, sortOpts } = this @@ -678,7 +662,7 @@ const Methods = { let sortColumns = visibleColumn.filter(({ order }) => !!order) if (sortColumns.length > 1) { - tableData = sortMultiple(tableData, sortColumns, this) + tableData = this.sortMultiple(tableData, sortColumns, this) sortedFlag = true } } @@ -713,13 +697,13 @@ const Methods = { return tableData }, getRowById(rowid) { - let { fullDataRowIdData } = this - let rowCache = fullDataRowIdData[rowid] + const { fullDataRowIdData } = this + const rowCache = fullDataRowIdData[rowid] return rowCache ? rowCache.row : null }, // 获取处理后的表格数据 getTableData() { - let { afterFullData, footerData, tableData, tableFullData } = this + const { afterFullData, footerData, tableData, tableFullData } = this return { visibleData: afterFullData.slice(0), footerData: footerData.slice(0), @@ -735,7 +719,6 @@ const Methods = { this.expandConfig && this.handleDefaultRowExpand() this.treeConfig && this.handleDefaultTreeExpand() this.updateFooter() - this.$nextTick(() => setTimeout(this.recalculate)) }, // 动态列处理 mergeCustomColumn(customColumns, sort, colWidth) { @@ -784,7 +767,8 @@ const Methods = { this.$emit('update:customs', fullColumn) }, resetAll() { - run(['resetCustoms', 'resetResizable'], this) + this.resetCustoms() + this.resetResizable() }, hideColumn(tableColumn) { // 返回隐藏的列 @@ -922,7 +906,6 @@ const Methods = { return this.$nextTick() .then(() => { this.updateFooter() - this.recalculate() }) .then(() => { // 在列初始化、列动态改变后都会抛出 @@ -935,33 +918,64 @@ const Methods = { }, // 指定列宽的列进行拆分 analyColumnWidth() { - let { columnMinWidth, columnStore, columnWidth, tableFullColumn } = this - let [autoList, pxList, pxMinList, resizeList, scaleList, scaleMinList] = [[], [], [], [], [], []] - let ruleChains = [ + // 从实例中获取列相关的配置参数 + const { columnMinWidth, columnStore, columnWidth, tableFullColumn } = this + + // 初始化不同类型列的数组: + // autoList - 自适应宽度的列 + // pxList - 固定像素宽度的列 + // pxMinList - 最小像素宽度的列 + // resizeList - 用户手动调整过宽度的列 + // scaleList - 百分比宽度的列 + // scaleMinList - 最小百分比宽度的列 + const [autoList, pxList, pxMinList, resizeList, scaleList, scaleMinList] = [[], [], [], [], [], []] + + // 定义列宽度规则链,按优先级从高到低排序 + const ruleChains = [ { + // 规则1: 用户手动调整过宽度的列 match: (col) => col.resizeWidth, action: (col) => resizeList.push(col) }, - { match: (col) => isPx(col.width), action: (col) => pxList.push(col) }, { + // 规则2: 设置了固定像素宽度的列 + match: (col) => isPx(col.width), + action: (col) => pxList.push(col) + }, + { + // 规则3: 设置了百分比宽度的列 match: (col) => isScale(col.width), action: (col) => scaleList.push(col) }, { + // 规则4: 设置了最小像素宽度的列 match: (col) => isPx(col.minWidth), action: (col) => pxMinList.push(col) }, { + // 规则5: 设置了最小百分比宽度的列 match: (col) => isScale(col.minWidth), action: (col) => scaleMinList.push(col) }, - { match: () => true, action: (col) => autoList.push(col) } + { + // 规则6: 默认规则,将列加入自适应列表 + match: () => true, + action: (col) => autoList.push(col) + } ] + + // 遍历所有列,根据规则链分配到对应的列表中 for (let i = 0; i < tableFullColumn.length; i++) { let column = tableFullColumn[i] + + // 如果列没有设置宽度,使用全局columnWidth columnWidth && !column.width && (column.width = columnWidth) + // 如果列没有设置最小宽度,使用全局columnMinWidth columnMinWidth && !column.minWidth && (column.minWidth = columnMinWidth) + + // 只处理可见的列 if (column.visible) { + // 遍历规则链,匹配第一个符合的规则 for (let j = 0; j < ruleChains.length; j++) { let ruleChain = ruleChains[j] if (ruleChain.match(column)) { @@ -971,6 +985,8 @@ const Methods = { } } } + + // 更新columnStore中的列分类数据 Object.assign(columnStore, { autoList, pxList, pxMinList, resizeList, scaleList, scaleMinList }) }, @@ -985,29 +1001,13 @@ const Methods = { if (isDisplayNone(mainBody)) { return } - - const { scrollXLoad, scrollYLoad, scrollLoad } = this const { tableBody, tableFooter, tableHeader } = this.$refs const getElem = (ref) => (ref ? ref.$el : null) const headerElem = getElem(tableHeader) const bodyElem = getElem(tableBody) const footerElem = getElem(tableFooter) - - if (!bodyElem) { - return this.computeScrollLoad() - } - // 设置表格每列的尺寸(此时还没有设置colgroup的dom元素尺寸),这里执行之后还需要继续设置滚动条状态 this.autoCellWidth(headerElem, bodyElem, footerElem) - - if (scrollXLoad || scrollYLoad || scrollLoad) { - return this.computeScrollLoad().then(() => { - this.autoCellWidth(headerElem, bodyElem, footerElem) - }) - } - - // 实现布局,将列renderWidth设置到具体的dom上 - return this.computeScrollLoad() }, // 列宽计算 autoCellWidth(headerEl, bodyEl, footerEl) { @@ -1047,20 +1047,14 @@ const Methods = { generateFixedClassName({ $table: this, bodyElem: bodyEl, leftList, rightList }) } }, - // 同步headerHeight - syncHeaderHeight() { - let headerEl = this.$refs.tableHeader?.$el - if (headerEl) this.headerHeight = headerEl.offsetHeight - }, resetResizable() { const toolbarVm = this.getVm('toolbar') this.visibleColumn.forEach((col) => (col.resizeWidth = 0)) if (toolbarVm) { toolbarVm.resetResizable() } - this.analyColumnWidth() - return this.recalculate() + return this.analyColumnWidth() }, updateStyle() { let { columnStore, currentRow, height, maxHeight, minHeight, parentHeight, tableColumn, scrollbarWidth } = this @@ -1090,8 +1084,6 @@ const Methods = { minHeight = ret.minHeight }) currentRow && this.setCurrentRow(currentRow) - // Fixed issue #129 - this.syncHeaderHeight() return this.$nextTick(() => { const { leftList, rightList } = columnStore // 只有在虚拟滚动+冻结列同时存在的情况下才重新计算冻结列位置 @@ -1120,7 +1112,7 @@ const Methods = { let blurClass = [] if (typeof blurClassConfig === 'string') { blurClass.push(blurClassConfig) - } else if (isArray(blurClassConfig)) { + } else if (Array.isArray(blurClassConfig)) { blurClass = blurClassConfig.slice(0) } @@ -1150,29 +1142,6 @@ const Methods = { handleGlobalKeydownEvent, handleGlobalResizeEvent, handleGlobalMousedownCaptureEvent, - // 处理单选框默认勾选 - handleRadioDefChecked() { - let { fullDataRowIdData } = this - let { checkRowKey } = this.radioConfig || {} - let rowid = checkRowKey && encodeURIComponent(checkRowKey) - let rowCache = fullDataRowIdData[rowid] - if (rowid && rowCache) { - this.setRadioRow(rowCache.row) - } - }, - // 单选,行选中事件 - triggerRadioRowEvent(event, params) { - let { selectRow } = this - let { checkMethod } = this.radioConfig || {} - if (checkMethod && !checkMethod(params)) { - return - } - this.setRadioRow(params.row) - this.setCurrentRow(params.row) - if (selectRow !== params.row) { - emitEvent(this, 'radio-change', [params, event]) - } - }, triggerCurrentRowEvent(event, params) { let { currentRow } = this this.setCurrentRow(params.row) @@ -1182,7 +1151,8 @@ const Methods = { }, // 高亮行,设置某一行为高亮状态,如果调不加参数,则会取消目前高亮行的选中状态 setCurrentRow(row) { - run(['clearCurrentRow', 'clearCurrentColumn'], this) + this.clearCurrentRow() + this.clearCurrentColumn() this.currentRow = row if (this.highlightCurrentRow) { let rowElems = this.$el.querySelectorAll(`[data-rowid="${getRowid(this, row)}"]`) @@ -1190,30 +1160,16 @@ const Methods = { } return this.$nextTick() }, - setRadioRow(row) { - row !== this.selectRow && this.clearRadioRow() - this.selectRow = row - return this.$nextTick() - }, clearCurrentRow() { Object.assign(this, { currentRow: null, hoverRow: null }) let rowElems = this.$el.querySelectorAll('.row__current') arrayEach(rowElems, (elem) => removeClass(elem, 'row__current')) return this.$nextTick() }, - clearRadioRow() { - let { selectRow: radioRow } = this - radioRow && (this.selectRow = null) - return this.$nextTick() - }, getCurrentRow() { let { currentRow } = this return currentRow }, - getRadioRow() { - let { selectRow: radioRow } = this - return radioRow - }, triggerHeaderCellClickEvent(event, params) { let { _lastResizeTime: lastTime, highlightCurrentColumn } = this let { cell, column } = params @@ -1241,7 +1197,8 @@ const Methods = { return this.$nextTick() }, setCurrentColumn(column) { - run(['clearCurrentRow', 'clearCurrentColumn'], this) + this.clearCurrentRow() + this.clearCurrentColumn() this.currentColumn = column let colElems = this.$el.querySelectorAll(`.${column.id}`) arrayEach(colElems, (elem) => addClass(elem, 'col__current')) @@ -1321,470 +1278,52 @@ const Methods = { emitEvent(this, 'cell-dblclick', [params, event]) }, - // 点击排序事件 - triggerSortEvent(event, column, order) { - let property = column.property - let isColumnSortable = column.type ? false : column.sortable || column.remoteSort - if (this.sortable && isColumnSortable) { - let evntParams = { $table: this, column, order, property } - - evntParams.prop = property - evntParams.field = evntParams.prop - - if (order === column.order) { - evntParams.order = null - this.clearSort(column.property) - } else { - this.sort(property, order) - } - emitEvent(this, 'sort-change', [evntParams, event]) - } - }, - sort(field, order) { - let { remoteSort, tableFullColumn, visibleColumn } = this - let column = find(visibleColumn, (item) => item.property === field) - let isRemote = isBoolean(column.remoteSort) ? column.remoteSort : remoteSort - let isColumnSortable = column.type ? false : column.sortable || column.remoteSort - - if (this.sortable && isColumnSortable) { - if (column.order !== order) { - tableFullColumn.forEach((column) => (column.order = null)) - column.order = order - // 如果是服务端排序,则跳过本地排序处理 - !isRemote && this.handleTableData(true).then(this.refreshStyle) - } - return this.$nextTick().then(this.updateStyle) - } - return this.$nextTick() - }, - clearSort() { - arrayEach(this.tableFullColumn, (column) => (column.order = null)) - this.$grid && (this.$grid.sortData = {}) - - return this.handleTableData(true).then(this.refreshStyle) - }, - toggleGroupExpansion(row) { - this.groupExpandeds.push(row) - }, - // 展开行事件 - triggerRowExpandEvent(event, { row }) { - let rest = this.toggleRowExpansion(row) - let eventParams = { $table: this, row, rowIndex: this.getRowIndex(row) } - emitEvent(this, 'toggle-expand-change', [eventParams, event]) - return rest - }, - // 切换展开行 - toggleRowExpansion(row) { - return this.setRowExpansion(row) - }, - // 处理默认展开行 - handleDefaultRowExpand() { - let { fullDataRowIdData, tableFullData } = this - let { expandAll, expandRowKeys } = this.expandConfig || {} - if (expandAll) { - this.expandeds = tableFullData.slice(0) - return - } - if (expandRowKeys) { - let defExpandeds = [] - expandRowKeys.forEach((rowid) => { - let rowCache = fullDataRowIdData[rowid] - rowCache && defExpandeds.push(rowCache.row) - }) - this.expandeds = defExpandeds - } - }, - setAllRowExpansion(expanded) { - this.expandeds = !expanded ? [] : this.tableFullData.slice(0) - return this.$nextTick().then(this.recalculate) - }, - // 设置展开行,二个参数设置这一行展开与否;支持单行;支持多行 - setRowExpansion(rows, expanded) { - let { expandeds } = this - let { accordion } = this.expandConfig || {} - - // 是否是切换模式 - let isToggle = arguments.length === 1 - - // 手风琴模式是否关闭了所有展开行 - let isAccordionCloseAll = false - - if (!rows) { - return this.$nextTick().then(this.recalculate) - } - if (!isArray(rows)) { - rows = [rows] - } - // 手风琴模式只能同时展开一个 - if (accordion) { - rows = rows.slice(rows.length - 1, rows.length) - - // 如果是手风琴模式,则需要判断是当前切换时关闭还是展开,解决手风琴模式无法关闭当前行的问题 - if (rows.length && isToggle) { - isAccordionCloseAll = expandeds.includes(rows[0]) - } - expandeds.length = 0 - } - - rows.forEach((row) => { - let index = expandeds.indexOf(row) - // 切换模式下此行已展开,或者非切换模式下合起已展开的行,就合起此行 - if ((isToggle && ~index) || (!isToggle && !expanded && ~index)) { - expandeds.splice(index, 1) - // 直接返回,因为合起了不会又要展开 - return - } - // 切换模式下此行未展开,或者非切换模式下展开已合起的行,就展开此行 - if ((isToggle && !isAccordionCloseAll && !~index) || (!isToggle && expanded && !~index)) { - expandeds.push(row) - } - }) - return this.$nextTick().then(this.recalculate) - }, - hasRowExpand(row) { - return ~this.expandeds.indexOf(row) - }, - clearRowExpand() { - let hasExpand = this.expandeds.length - this.expandeds = [] - return this.$nextTick().then(() => (hasExpand ? this.recalculate() : 0)) - }, - // 获取虚拟滚动状态 - getVirtualScroller() { - let { scrollXLoad, scrollYLoad } = this - let { scrollLeft, scrollTop } = this.$refs.tableBody.$el - return { - scrollX: scrollXLoad, - scrollY: scrollYLoad, - scrollLeft, - scrollTop - } - }, - // 横向 X 可视渲染事件处理 - triggerScrollXEvent(event) { - this.loadScrollXData(event) - }, - debounceScrollX(event) { - if (!this.tasks.debounceScrollX) { - this.tasks.debounceScrollXHandler = null - this.tasks.debounceScrollX = () => { - return requestAnimationFrame(() => { - this.tasks.debounceScrollXHandler = null - this.loadScrollXData(event) - }) - } - } - - if (this.tasks.debounceScrollXHandler) { - cancelAnimationFrame(this.tasks.debounceScrollXHandler) - this.tasks.debounceScrollXHandler = null - } - - this.tasks.debounceScrollXHandler = this.tasks.debounceScrollX() - }, - // 处理x轴滚动时,虚拟滚动数据计算 - loadScrollXData() { - let { scrollXStore, visibleColumn } = this - let { offsetSize, renderSize, startIndex, visibleIndex, visibleSize } = scrollXStore - let { scrollLeft } = this.$refs.tableBody.$el - let { preload = false, toVisibleIndex = 0, width = 0 } = {} - // 根据滚动位置计算边界可见列 - for (let i = 0; i < visibleColumn.length; i++) { - width += visibleColumn[i].renderWidth - if (scrollLeft < width) { - toVisibleIndex = i // 边界可见列索引 - break - } - } - // 边界可见列和上次记录的相同,滚动还没超过此列,就关闭Tooltip退出 - if (visibleIndex === toVisibleIndex) { - this.clostTooltip() - return - } - let marginSize = Math.min(Math.floor((renderSize - visibleSize) / 2), visibleSize) - marginSize = Math.max(0, marginSize) - if (visibleIndex > toVisibleIndex) { - // 向左 - preload = startIndex >= toVisibleIndex - offsetSize - if (preload) { - scrollXStore.startIndex = Math.max(0, toVisibleIndex - Math.max(marginSize, renderSize - visibleSize)) - this.updateScrollXData() - } - } else { - // 向右 - preload = startIndex + renderSize <= toVisibleIndex + visibleSize + offsetSize - if (preload) { - scrollXStore.startIndex = Math.max(0, Math.min(visibleColumn.length - renderSize, toVisibleIndex - marginSize)) - this.updateScrollXData() - } - } - scrollXStore.visibleIndex = toVisibleIndex - this.clostTooltip() - }, - // 纵向 Y 可视渲染事件处理 - triggerScrollYEvent(event) { - this.loadScrollYData(event) - }, - // 处理滚动分页相关逻辑 - debounceScrollLoad(event) { - if (!this.tasks.debounceScrollLoad) { - this.tasks.debounceScrollLoad = debounce(debounceScrollLoadDuration, () => { - const { scrollHeight, bodyHeight } = this.scrollLoadStore - const { currentPage, pageSize } = this.$grid.tablePage - const max = scrollHeight - bodyHeight - let scrollTop = event.target.scrollTop - - if (scrollTop > max) { - scrollTop = max - } - - const { rowHeight } = this.scrollYStore - let visibleIndex = Math.ceil(scrollTop / rowHeight) - let page = Math.ceil(visibleIndex / pageSize) + 1 - - if (currentPage !== page) { - this.$grid.pageCurrentChange(page) - } - }) - } - - this.tasks.debounceScrollLoad() - }, - // 纵向 Y 可视渲染处理 - loadScrollYData(event) { - const { scrollYStore } = this as any - const { startIndex, renderSize, offsetSize, visibleIndex, visibleSize, rowHeight } = scrollYStore - - // 动态获取容器的scrollTop,这里有可能会造成卡顿,暂时没有好的方案 - let { scrollTop } = event.target - let toVisibleIndex = Math.ceil(scrollTop / rowHeight) - let preload = false - if (visibleIndex === toVisibleIndex) { - return - } - let marginSize = Math.min(Math.floor((renderSize - visibleSize) / 2), visibleSize) - if (toVisibleIndex < visibleIndex) { - // 向上 - preload = startIndex >= toVisibleIndex - offsetSize - if (preload) { - scrollYStore.startIndex = Math.max(0, toVisibleIndex - Math.max(marginSize, renderSize - visibleSize)) - this.updateScrollYData() - } - } else { - // 向下 - preload = startIndex + renderSize <= toVisibleIndex + visibleSize + offsetSize - if (preload) { - let totalRows = getTotalRows(this) - scrollYStore.startIndex = Math.max(0, Math.min(totalRows - renderSize, toVisibleIndex - marginSize)) - this.updateScrollYData() - } - } - scrollYStore.visibleIndex = toVisibleIndex - this.$nextTick(() => { - this.updateSelectedCls(true) - }) - }, getRowHeight() { - const { $refs, vSize } = this - const { scrollY } = this.optimizeOpts - let { tableBody, tableHeader } = $refs - let rHeight = scrollY.rHeight - if (!rHeight) { - // 获取表头或者表格体第一个tr的高度 - let firstTrElem = - (tableBody && tableBody.$el.querySelector('tbody>tr')) || - (tableHeader && tableHeader.$el.querySelector('thead>tr')) || - null - if (firstTrElem) { - rHeight = firstTrElem.clientHeight - } - } - // 默认的行高,默认行高需要跟 css 样式一致 - if (!rHeight) { - let vSizeList = ['medium', 'small', 'mini'] - // 这里因为需要适配多套主题配置方案,所以这里的默认高度写死不合适,待整改 - let defSizeList = [44, 40, 36] - let i = vSizeList.indexOf(vSize) - rHeight = ~i ? defSizeList[i] : 48 - } - return rHeight - }, - // 计算可视渲染相关数据 - computeScrollLoad() { - return this.$nextTick().then(() => { - let { $refs, optimizeOpts, visibleColumn } = this as any - let { scrollLoad, scrollXLoad, scrollXStore, scrollYLoad, scrollYStore } = this as any - let { scrollX, scrollY } = optimizeOpts - let { tableBody } = $refs - let bodyElem = tableBody ? tableBody.$el : null - if (bodyElem) { - // 只计算X轴虚拟滚动逻辑,优化正常表格计算效率 - computeScrollXLoad({ _vm: this, scrollX, scrollXLoad, scrollXStore, tableBodyElem: bodyElem, visibleColumn }) - // 只计算Y轴虚拟滚动逻辑,优化正常表格计算效率 - computeScrollYLoad({ _vm: this, scrollLoad, scrollY, scrollYLoad, scrollYStore, tableBodyElem: bodyElem }) - } - this.$nextTick(this.updateStyle) - }) - }, - // 处理x轴方向虚拟滚动列数据加载 - updateScrollXData() { - let { scrollXStore } = this - - // 获取需要渲染的列数和最后一次渲染列的index值 - let ret = sliceVisibleColumn(this) - - if (ret.sliced) { - // 更新DOM样式保证表格滚动时的对齐,初始化表格时也需要计算x轴方向滚动条占位符的尺寸 - this.updateScrollXSpace() - // 处理滚动条滚动后的异步渲染列逻辑 - this.updateScrollStatus() - } - - this.debounceRaf('updateScrollXDataHandler', () => { - scrollXStore.lastStartIndex = ret.lastStartIndex - // 设置新的渲染列触发Vue渲染 - this.tableColumn = ret.tableColumn - this.visibleColumnChanged = ret.visibleColumnChanged - this.$nextTick(() => { - this.updateFooter() - this.updateStyle() - }) - }) - }, - // 更新横向 X 可视渲染上下剩余空间大小 - updateScrollXSpace() { - const { elemStore, scrollXLoad, scrollXStore, scrollbarWidth, tableWidth, visibleColumn } = this - const { startIndex } = scrollXStore - let { bodyElem, footerElem, headerElem, leftSpaceWidth, marginLeft } = {} - - // 从缓存中获取主表头/主表体/主表尾表格元素 - headerElem = elemStore['main-header-table'] - bodyElem = elemStore['main-body-table'] - footerElem = elemStore['main-footer-table'] - - // 累加已滚动出渲染范围的列的总渲染宽度 - leftSpaceWidth = visibleColumn.slice(0, startIndex).reduce((previous, column) => { - // 左侧冻结列,不计算margin - if (column.fixed === 'left') return previous - return previous + column.renderWidth - }, 0) - marginLeft = scrollXLoad ? `${leftSpaceWidth}px` : '' - - // 设置主表头/主表体/主表尾表格元素的marginLeft(已滚动出渲染范围的列,不渲染但是保留宽度占位,保证对齐) - headerElem && (headerElem.style.marginLeft = marginLeft) - bodyElem.style.marginLeft = marginLeft - footerElem && (footerElem.style.marginLeft = marginLeft) - const layouts = ['header', 'body', 'footer'] - layouts.forEach((layout) => { - const xSpaceElem = elemStore[`main-${layout}-xSpace`] - const extra = layout === 'header' ? scrollbarWidth : 0 - // 这里只能找到body中的元素,header和footer永远是false - if (xSpaceElem) { - // 表格主体内容x轴方向虚拟滚动条占位元素 - xSpaceElem.style.width = scrollXLoad ? `${tableWidth + extra}px` : '' - } - }) - - this.$nextTick(this.updateStyle) - }, - debounceRaf(handlerKey, callback) { - if (this[handlerKey]) { - cancelAnimationFrame(this[handlerKey]) - } - - this[handlerKey] = requestAnimationFrame(() => { - this[handlerKey] = null - callback() - }) - }, - // 处理虚拟滚动加载数据,并更新YSpace位置 - updateScrollYData() { - // 更新DOM样式保证表格滚动时的对齐 - this.updateScrollYSpace() - // 节流更新响应数据 - this.debounceRaf('updateScrollYDataHandler', () => { - this.handleTableData().then(() => this.$nextTick(this.updateStyle)) - }) - }, - // 更新纵向虚拟滚动 Y 可视渲染上下剩余空间大小(使用tiny-grid-body__y-space元素撑开足够空间) - updateScrollYSpace() { - let { $grid, elemStore, scrollLoad, scrollLoadStore, scrollYLoad } = this - let { rowHeight, startIndex } = this.scrollYStore - let totalRows = getTotalRows(this) - let bodyHeight = totalRows * rowHeight - let scrollHeight = $grid.pagerConfig ? $grid.pagerConfig.total * rowHeight : 0 - let isVScrollOrLoad = scrollYLoad || scrollLoad - let { marginTop, ySpaceHeight } = {} - - // 通过开始渲染下标startIndex和表格的行高度来计算marginTop,虚滚场景为已滚动出渲染范围的行的总高度,滚动分页场景为空 - marginTop = isVScrollOrLoad && scrollYLoad ? `${Math.max(startIndex * rowHeight, 0)}px` : '' - // 虚滚场景的滚动高度,滚动分页场景的视口高度 - ySpaceHeight = isVScrollOrLoad ? `${bodyHeight}px` : '' - - // 滚动分页场景的视口高度和滚动高度缓存 - scrollLoadStore.bodyHeight = bodyHeight - scrollLoadStore.scrollHeight = scrollHeight - - const tableElem = elemStore['main-body-table'] - - // 这里最好使用transform3D,使用gpu加速,防止页面重绘 - if (tableElem) { - tableElem.style.transform = marginTop ? `translateY(${marginTop})` : '' - } - - const ySpaceElem = elemStore['main-body-ySpace'] - ySpaceElem && (ySpaceElem.style.height = ySpaceHeight) - - // 滚动分页加载逻辑 - if (ySpaceElem && scrollLoad && $grid) { - Object.assign(scrollLoadStore, { bodyHeight, scrollHeight }) - ySpaceElem.firstChild.style.height = `${scrollHeight}px` - ySpaceElem.onscroll = this.debounceScrollLoad - } - }, - updateScrollLoadBar(event) { - let { $el, elemStore, scrollLoad, scrollLoadStore } = this - if (scrollLoad && $el.contains(event.target)) { - let wheelDelta = event.wheelDelta ? event.wheelDelta : -event.detail * 40 - let scrollElm = elemStore['main-body-ySpace'] - let { scrollHeight, bodyHeight } = scrollLoadStore - let max = scrollHeight - bodyHeight - let top = scrollElm.scrollTop - wheelDelta - top = max < top ? max : top - top = top < 0 ? 0 : top - scrollElm.scrollTop = top - } + return this.rowHeight }, + // 滚动到指定位置 scrollTo(scrollLeft, scrollTop) { const { elemStore } = this const tableBodyElem = elemStore['main-body-wrapper'] const tableHeaderElem = elemStore['main-header-wrapper'] const tableFooterElem = elemStore['main-footer-wrapper'] + + // 滚动到指定的水平位置 if (isNumber(scrollLeft)) { tableBodyElem && (tableBodyElem.scrollLeft = scrollLeft) tableFooterElem && (tableFooterElem.scrollLeft = scrollLeft) tableHeaderElem && (tableHeaderElem.scrollLeft = scrollLeft) } + + // 滚动到指定的垂直位置 if (isNumber(scrollTop)) { tableBodyElem && (tableBodyElem.scrollTop = scrollTop) } + return this.$nextTick() }, + // 滚动到指定行 scrollToRow(row, column, isDelay, move) { let hasRowCache = this.fullAllDataRowMap.has(row) let isDelayArg = isDelay || isBoolean(column) + // 如果存在行,滚动到可见位置 row && hasRowCache && rowToVisible(this, row) return this.scrollToColumn(column, isDelayArg, move) }, + // 滚动到树形结构的指定行 scrollToTreeRow(row) { let { tableFullData, treeConfig, treeOpts } = this if (!treeConfig) { return this.$nextTick() } + + // 查找目标行在树中的路径 let matchObj = findTree(tableFullData, (item) => item === row, treeOpts) if (!matchObj) { return this.$nextTick() } + + // 展开路径上的所有节点 let nodes = matchObj.nodes nodes.forEach((row, index) => { if (index === nodes.length - 1 || this.hasTreeExpand(row)) { @@ -1792,260 +1331,174 @@ const Methods = { } this.setTreeExpansion(row, true) }) + return this.$nextTick() }, + // 滚动到指定列 scrollToColumn(column, isDelay, move) { let hasColCache = this.fullColumnMap.has(column) + // 如果列存在,滚动到可见位置 column && hasColCache && colToVisible(this, column, move) - // 虚滚场景 DOM 元素会延时渲染,DOM 元素不存在时校验会显示异常 + // 虚拟滚动场景下,DOM元素延迟渲染,需要额外等待 return isDelay && (this.scrollXLoad || this.scrollYLoad) ? new Promise((resolve) => setTimeout(() => resolve(this.$nextTick()), 50)) : this.$nextTick() }, + // 重置滚动顶部位置 resetScrollTop() { this.lastScrollTop = 0 }, + // 清除滚动状态 clearScroll() { let { scrollXStore, scrollYStore, elemStore } = this + // 重置滚动位置记录 Object.assign(this, { lastScrollLeft: 0, lastScrollTop: 0 }) + // 重置虚拟滚动存储 Object.assign(scrollXStore, { startIndex: 0, visibleIndex: 0 }) Object.assign(scrollYStore, { startIndex: 0, visibleIndex: 0 }) + this.$nextTick(() => { - // 从缓存中拿 DOM 元素 + // 从缓存中获取DOM元素 const tableBodyElem = elemStore['main-body-wrapper'] const tableHeaderElem = elemStore['main-header-wrapper'] const tableFooterElem = elemStore['main-footer-wrapper'] + // 重置所有区域的滚动位置 if (this.afterMounted) { tableBodyElem && Object.assign(tableBodyElem, { scrollLeft: 0, scrollTop: 0 }) tableFooterElem && Object.assign(tableFooterElem, { scrollLeft: 0 }) tableHeaderElem && Object.assign(tableHeaderElem, { scrollLeft: 0 }) } }) + return this.$nextTick() }, - // 更新表尾合计 + // 更新表尾合计行 updateFooter() { let { afterFullData, footerMethod, showFooter, summaryConfig, tableColumn } = this + + // 如果设置了自定义表尾方法 if (footerMethod && showFooter) { let data = footerMethod({ columns: tableColumn, data: afterFullData }) - if (data.length && data.some((value) => !isArray(value))) { + // 如果返回的不是二维数组,转换为二维数组 + if (data.length && data.some((value) => !Array.isArray(value))) { data = [data] } this.footerData = tableColumn.length ? data : [] } + + // 如果设置了汇总配置 if (summaryConfig) { let { fields, fraction, text, truncate } = summaryConfig + // 构建汇总行数据 let summary = tableColumn.map((column, columnIndex) => { if (columnIndex === 0) { - return text || '' + return text || '' // 第一列显示汇总文本 } if (~fields.indexOf(column.property)) { - return toDecimal(sum(this.afterFullData, column.property), fraction, truncate) + return toDecimal(sum(this.afterFullData, column.property), fraction, truncate) // 计算汇总值 } return null }) this.footerData = [summary] } + return this.$nextTick() }, // 更新列状态:如果组件值v-model发生change,调用该函数更新列的编辑状态。如果单元格配置了校验规则,则进行校验 updateStatus(scope, cellValue, renderOpts) { - let customValue = !isUndefined(cellValue) - return this.$nextTick().then(() => { - let { $refs, editRules, tableData, validStore } = this - let { tableBody } = $refs - if (!scope || !tableBody || !editRules) { - return - } + let { $refs, editRules, tableData, validStore } = this + let { tableBody } = $refs + if (!scope || !tableBody) { + return this.$nextTick() + } - if (renderOpts && renderOpts.isValidAlways) { - validStore.visible = true - } + let { column, row } = scope + let type = 'change' - let { column, row } = scope - let type = 'change' - if (!this.hasCellRules(type, row, column)) { - return + let refreshStatus = () => { + if (!isUndefined(cellValue)) { + this.updateRowStatus(row) + this.$refs.tableBody?.$forceUpdate() } - let rowIndex = tableData.indexOf(row) - getCell(this, { row, rowIndex, column }).then((cell) => { - if (!cell) { - return - } - return this.validCellRules(type, row, column, cellValue) - .then(() => { - customValue && validStore.visible && setCellValue(row, column, cellValue) - this.clearValidate() - }) - .catch(({ rule }) => { - customValue && setCellValue(row, column, cellValue) - this.showValidTooltip({ rule, row, column, cell }) - }) - }) - }) - }, - /* X/Y 方向滚动 */ - updateScrollStatus() { - if (!this.tasks.updateScrollStatus) { - this.tasks.updateScrollStatus = debounce(AsyncCollectTimeout, () => { - const { scrollXLoad, scrollYLoad, isAsyncColumn } = this - - if (isAsyncColumn && (scrollXLoad || scrollYLoad)) { - const { tableData, scrollXStore, scrollYStore, tableFullData, scrollDirection = 'N' } = this - const isInit = - (scrollXLoad && scrollXStore.visibleIndex === 0) || (scrollYLoad && scrollYStore.visibleIndex === 0) - - // 第一次初始化及横、纵向滚动时(用户直接设置 data 属性时将由 handleAsyncColumn 初始化异步列) - if (isInit || scrollDirection !== 'N') { - this.handleResolveColumn(tableFullData, this.collectAsyncColumn(tableData)) - } - } - }) + } + // 如果没有相关校验规则,直接返回 + if (!editRules || !this.hasCellRules(type, row, column)) { + refreshStatus() + return this.$nextTick() } - this.tasks.updateScrollStatus() - }, - - // 获取异步列唯一ID - getAsyncColumnUniqueKey(property, row) { - return `${property}_${row[this.rowId]}` - }, - // 获取异步列名称 - getAsyncColumnName(property) { - return GlobalConfig.constant.asyncPrefix + property - }, - // 收集异步列 - collectAsyncColumn(tableData) { - const fetchColumns = [] - const { rowId, asyncRenderMap, tableColumn } = this - if (!rowId) { - warn('The (grid-props:rowId) is required for the asynchronous column.') - return fetchColumns + // 如果设置了始终验证 + if (renderOpts && renderOpts.isValidAlways) { + validStore.visible = true } - tableColumn.forEach((col) => { - const { async } = col.format || {} - const { fetch, splitConfig = {} } = async || {} - if (typeof fetch === 'function') { - const columnValues = [] - tableData.forEach((row) => { - let cellValue = row[col.property] - if (typeof cellValue !== 'string' || (typeof cellValue === 'string' && !cellValue)) { - cellValue = ' ' - } - let cellValuesCount = 1 - let cellValues = [cellValue] - const uniqueKey = this.getAsyncColumnUniqueKey(col.property, row) - // 默认不开启 - if (splitConfig.enabled === true) { - cellValues = cellValue.split(splitConfig.valueSplit || ',') - cellValuesCount = cellValues.length - } - if (!asyncRenderMap[uniqueKey]) { - // 以行主键、列名作为缓存的 Key 防止重复加载(缓存单元格显示值的个数) - asyncRenderMap[uniqueKey] = cellValuesCount - // 单元格多值支持 - cellValues.forEach((value) => columnValues.push(value)) - } - }) - if (columnValues.length) { - fetchColumns.push({ ...col, columnValues }) - } + + // 获取单元格元素并执行校验 + let rowIndex = tableData.indexOf(row) + getCell(this, { row, rowIndex, column }).then((cell) => { + if (!cell) { + return } + return this.validCellRules(type, row, column, cellValue) + .then(() => { + // 校验通过,设置新值并清除验证提示 + refreshStatus() + this.clearValidate() + }) + .catch(({ rule }) => { + refreshStatus() + this.showValidTooltip({ rule, row, column, cell }) + }) }) - return fetchColumns - }, - // fetchData 执行 - handleAsyncColumn(tableData) { - if (this.isAsyncColumn && tableData.length) { - // 每次请求都需要清空加载缓存 - this.asyncRenderMap = {} - this.handleResolveColumn(tableData, this.collectAsyncColumn(tableData)) - } - }, - // 查询异步列 - handleResolveColumn(tableData, fetchColumns) { - const { tableColumn, scrollYStore, asyncRenderMap, scrollXLoad, scrollYLoad } = this - const { startIndex } = scrollYStore - const isScrollLoad = scrollXLoad || scrollYLoad - if (fetchColumns.length === 0) { - return - } - const promises = mapFetchColumnPromise({ _vm: this, fetchColumns, tableColumn }) - Promise.all(promises).then( - handleAllColumnPromises({ startIndex, fetchColumns, tableData, asyncRenderMap, isScrollLoad }, this) - ) }, - // Publish methods 与工具栏对接 + + // 与工具栏组件对接 connect({ toolbar }) { this.$toolbar = toolbar }, + // 检查触发源是否属于目标节点 getEventTargetNode, + // 可见性改变事件处理 handleVisibilityChange(visible, entry) { if (visible) { + // 可见时更新高度和布局 this.updateParentHeight() - this.updateTableBodyHeight() - this.recalculate() } + // 触发可见性变化事件 emitEvent(this, 'visible-change', [{ $table: this, visible, entry }]) }, - // 更新表体高度 - updateTableBodyHeight() { - if (!this.tasks.updateTableBodyHeight) { - this.tasks.updateTableBodyHeight = () => { - fastdom.measure(() => { - const tableBodyElem = this.elemStore['main-body-wrapper'] - this.tableBodyHeight = tableBodyElem ? tableBodyElem.clientHeight : 0 - }) - } - } - - this.tasks.updateTableBodyHeight() - }, - // 按顺序切换列的排序状态(null --> asc --> desc --> null --> ...) - toggleColumnOrder(column) { - return column.order ? (column.order === 'asc' ? 'desc' : null) : 'asc' - }, - watchDataForVue3() { - if (isVue2) return - - const stopWatch = hooks.watch( - [() => this.data, () => this.data && this.data.length], - ([newData, newLength], [oldData, oldLength]) => { - // vue3下额外监控数组长度改变,解决push无响应等问题 - if (Array.isArray(this.data) && newData === oldData && newLength !== oldLength) { - this.handleDataChange() - } - } - ) - - hooks.onBeforeUnmount(() => stopWatch()) - }, + // 获取特定名称的vm实例 getVm(name) { return this.$grid.getVm(name) }, + + // 组装列配置 assembleColumns() { // 如果没有初始化任何列实例就不进行列组装 if (!this.isTagUsageSence) return assemColumn(this) }, + + // 验证列名是否有效 isValidCustomColumn(columnName) { return columnName && this.columnNames.includes(columnName) }, + + // 计算列集合的唯一键 computeCollectKey() { const columnIds = [] + // 遍历列树结构收集列ID const traverse = (columns) => { if (Array.isArray(columns) && columns.length > 0) { columns.forEach((column) => { columnIds.push(column.columnConfig.id) - traverse(column.childColumns) }) } @@ -2053,12 +1506,10 @@ const Methods = { traverse(this.childColumns) + // 生成逗号分隔的ID字符串作为键 return columnIds.join(',') }, - // 获取所有多选数据状态 - getAllSelection() { - return this.selection - }, + // 尝试恢复滚动位置,规范了最大滚动位置的取值 attemptRestoreScoll(options) { let { lastScrollTop, lastScrollLeft } = options || this @@ -2066,18 +1517,24 @@ const Methods = { const { scrollXLoad, scrollYLoad, elemStore } = this const tableBodyElem = elemStore['main-body-wrapper'] + // 如果有上次滚动位置并且表格主体存在 if ((lastScrollTop || lastScrollLeft) && tableBodyElem) { + // 使用fastdom避免布局抖动 fastdom.measure(() => { + // 计算最大滚动范围 const maxScrollTop = tableBodyElem.scrollHeight - tableBodyElem.offsetHeight const maxScrollLeft = tableBodyElem.scrollWidth - tableBodyElem.offsetWidth + // 确保滚动位置不超过最大范围 lastScrollTop = Math.min(lastScrollTop, maxScrollTop) lastScrollLeft = Math.min(lastScrollLeft, maxScrollLeft) + // 设置滚动位置 fastdom.mutate(() => { this.restoreScollFlag = true this.scrollTo(lastScrollLeft, lastScrollTop) + // 触发滚动事件更新虚拟滚动渲染 scrollXLoad && this.triggerScrollXEvent() scrollYLoad && this.triggerScrollYEvent({ target: { scrollTop: lastScrollTop } }) }) @@ -2085,11 +1542,13 @@ const Methods = { } return this.$nextTick() + }, + updateRowStatus(row) { + updateRowStatus(this, row) + }, + getCellStatus(row, column) { + getCellStatus(this, row, column) } } -funcs.forEach((name) => { - Methods[name] = function (...args) { - return this[`_${name}`] ? this[`_${name}`](...args) : null - } -}) + export default Methods diff --git a/packages/vue/src/grid/src/table/src/strategy.ts b/packages/vue/src/grid/src/table/src/strategy.ts index 27767bf1b0..2884096a54 100644 --- a/packages/vue/src/grid/src/table/src/strategy.ts +++ b/packages/vue/src/grid/src/table/src/strategy.ts @@ -1,22 +1,42 @@ +/** + * 表格策略相关工具函数 + * 包含表格行键管理、树形结构处理、虚拟滚动、分组等功能 + */ + import { hooks } from '@opentiny/vue-common' -import { getRowkey } from '@opentiny/vue-renderless/grid/utils' -import { arrayEach, isEqual } from '@opentiny/vue-renderless/grid/static' +import { getRowkey } from '../../utils/utils' +import { arrayEach, isEqual } from '../../utils/static' import { warn } from '../../tools' const { toRaw } = hooks -const TEMPORARY_CHILDREN = '_$children_' -const TEMPORARY_SHOW = '_$show_' -const ROWKEY_MAP = new WeakMap() -const TOTALROWS_MAP = new WeakMap() -const CHART_MAP = new WeakMap() -const VIRTUAL_ROW_KEY = '_$virtual_' +// 常量定义 +const TEMPORARY_CHILDREN = '_$children_' // 临时子节点属性名 +const TEMPORARY_SHOW = '_$show_' // 临时显示属性名 +const ROWKEY_MAP = new WeakMap() // 存储表格行键的WeakMap +const TOTALROWS_MAP = new WeakMap() // 存储表格总行数的WeakMap +const CHART_MAP = new WeakMap() // 存储树形结构图的WeakMap +const VIRTUAL_ROW_KEY = '_$virtual_' // 虚拟行标识符 -let rowUniqueId = 0 +let rowUniqueId = 0 // 行唯一ID计数器 +/** + * 生成行唯一ID + * @returns {string} 格式为 'row_数字' 的唯一ID + */ const getRowUniqueId = () => `row_${++rowUniqueId}` + +/** + * 检查数组是否有效(非空数组) + * @param {Array} arr - 待检查的数组 + * @returns {boolean} 是否为有效数组 + */ const isValidArray = (arr) => Array.isArray(arr) && arr.length +/** + * 设置表格行键 + * @param {Object} $table - 表格实例 + */ const setTableRowKey = ($table) => { if (!ROWKEY_MAP.has($table)) { ROWKEY_MAP.set($table, getRowkey($table)) @@ -25,6 +45,11 @@ const setTableRowKey = ($table) => { return ROWKEY_MAP.get($table) } +/** + * 获取表格行键 + * @param {Object} $table - 表格实例 + * @returns {string} 行键 + */ const getTableRowKey = ($table) => { if (!ROWKEY_MAP.has($table)) { setTableRowKey($table) @@ -33,6 +58,14 @@ const getTableRowKey = ($table) => { return ROWKEY_MAP.get($table) } +/** + * 获取单元格唯一键 + * @param {Object} params - 参数对象 + * @param {Object} params.$table - 表格实例 + * @param {Object} params.column - 列配置 + * @param {Object} params.row - 行数据 + * @returns {string} 单元格唯一键 + */ const getTableCellKey = ({ $table, column, row }) => { if (!ROWKEY_MAP.has($table)) { setTableRowKey($table) @@ -41,11 +74,20 @@ const getTableCellKey = ({ $table, column, row }) => { return `${row[ROWKEY_MAP.get($table)]}-${column.id}` } +/** + * 设置树形结构滚动Y轴缓存 + * @param {Object} _vm - 组件实例 + */ const setTreeScrollYCache = (_vm) => { setCacheChartMap(_vm) setTotalRows(_vm) } +/** + * 构建树形结构图 + * @param {Object} _vm - 组件实例 + * @returns {Array} 树形结构图 + */ const buildChart = (_vm) => { const { afterFullData, scrollYLoad, treeConfig, treeExpandeds } = _vm @@ -93,12 +135,12 @@ const sliceTreeData = (_vm) => { const chart = getCacheChartMap(_vm) const { scrollYStore, treeConfig } = _vm const { renderSize, startIndex } = scrollYStore - const subChart = chart.slice(startIndex, startIndex + renderSize) + const subChart = chart?.slice(startIndex, startIndex + renderSize) const subTree = [] const { temporaryChildren = TEMPORARY_CHILDREN, temporaryShow = TEMPORARY_SHOW } = treeConfig const pushIfNot = (arr, item) => !arr.includes(item) && arr.push(item) - subChart.forEach((chartItem) => { + subChart?.forEach((chartItem) => { const lastIndex = chartItem.length - 1 for (let i = lastIndex; i > -1; i--) { @@ -120,17 +162,16 @@ const sliceTreeData = (_vm) => { return subTree } -const sliceFullData = (_vm) => { - let { afterFullData, scrollYLoad, scrollYStore, treeConfig, hasVirtualRow, groupFullData } = _vm - let { renderSize, startIndex } = scrollYStore +const sliceFullData = (vm) => { + const { afterFullData, scrollYLoad, scrollYStore, treeConfig, hasVirtualRow, groupFullData } = vm + const { renderSize, startIndex } = scrollYStore let result - let fullData // 分组表场景使用groupFullData - fullData = hasVirtualRow ? groupFullData : afterFullData + const fullData = hasVirtualRow ? groupFullData : afterFullData if (scrollYLoad) { if (treeConfig) { - result = sliceTreeData(_vm) + result = sliceTreeData(vm) } else { result = fullData.slice(startIndex, startIndex + renderSize) } @@ -385,15 +426,28 @@ const sliceColumnTree = (_vm) => { const setSliceColumnTree = (_vm) => _vm.isGroup && (_vm._sliceColumnTree = sliceColumnTree(_vm)) -/** 判断是否是虚拟行 */ +/** + * 判断是否是虚拟行 + * @param {Object} row - 行数据 + * @returns {boolean} 是否为虚拟行 + */ const isVirtualRow = (row) => row && row[VIRTUAL_ROW_KEY] -/** 普通表分组场景,按数据顺序对数据进行分组 */ +/** + * 对表格数据进行分组处理 + * @param {Array} arr - 原始数据数组 + * @param {string} key - 分组键 + * @param {Function} equals - 比较函数 + * @param {Function} active - 激活状态判断函数 + * @param {string} rowKey - 行键 + * @returns {Array} 分组后的数据数组 + */ const orderingGroupBy = (arr, key, equals, active, rowKey) => { const result = [] const virtualItems = [] - // 虚拟行id计数 let virtualRowId = 0 + + // 创建虚拟行 const createVirtualItem = (vItem) => { vItem = { [VIRTUAL_ROW_KEY]: true, @@ -487,7 +541,7 @@ const handleRowGroupFold = (row, _vm) => { } _vm.groupFullData = copy - _vm.handleTableData().then(_vm.recalculate) + _vm.handleTableData() } export { diff --git a/packages/vue/src/grid/src/table/src/utils/autoCellWidth.ts b/packages/vue/src/grid/src/table/src/utils/autoCellWidth.ts index 13f4c720a9..449eb2984c 100644 --- a/packages/vue/src/grid/src/table/src/utils/autoCellWidth.ts +++ b/packages/vue/src/grid/src/table/src/utils/autoCellWidth.ts @@ -1,39 +1,22 @@ /** - * 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. - * + * 处理自适应列宽的函数 + * @param autoList - 需要自适应的列数组 + * @param meanWidth - 平均宽度 + * @param minCellWidth - 最小单元格宽度 + * @param tableWidth - 表格总宽度 + * @param fit - 是否需要填充满容器 + * @param bodyWidth - 表格容器宽度 */ - -// 自适应 -const adaptive = ({ autoArr, meanWidth, minCellWidth, tableWidth, fit, bodyWidth }) => { - autoArr.forEach((column, index) => { - let width = Math.max(meanWidth, minCellWidth) +const adaptive = ({ autoList, meanWidth, minCellWidth, tableWidth, fit, bodyWidth }) => { + autoList.forEach((column, index) => { + const width = Math.max(meanWidth, minCellWidth) column.renderWidth = width tableWidth += width - if (fit && index === autoArr.length - 1) { + if (fit && index === autoList.length - 1) { // 如果所有列足够放的情况下,修补列之间的误差 - let odiffer = bodyWidth - tableWidth + const odiffer = bodyWidth - tableWidth if (odiffer > 0) { column.renderWidth += odiffer @@ -45,77 +28,121 @@ const adaptive = ({ autoArr, meanWidth, minCellWidth, tableWidth, fit, bodyWidth return tableWidth } -// 计算每一列的渲染宽度:renderWidth +/** + * 初始化表格宽度,计算每一列的渲染宽度 + * @param remainWidth - 剩余可用宽度 + * @param columnStore - 列配置存储对象,包含不同类型的列数组 + * @returns {Object} 返回计算后的表格总宽度和平均宽度 + */ const initTableWidth = ({ remainWidth, columnStore }) => { + // 初始化表格总宽度 let tableWidth = 0 - let { resizeList: resizeArr, pxMinList: pxMinArr, pxList: pxArr } = columnStore - let { scaleList: scaleArr, scaleMinList: scaleMinArr } = columnStore - // 最小宽 - pxMinArr.forEach((column) => { - let minWidth = parseInt(column.minWidth) + // 从columnStore中解构出不同类型的列数组: + // resizeList - 用户手动调整过宽度的列 + // pxMinList - 设置了最小像素宽度(min-width="100px")的列 + // pxList - 设置了固定像素宽度(width="100px")的列 + const { resizeList, pxMinList, pxList } = columnStore + + // scaleList - 设置了百分比宽度(width="20%")的列 + // scaleMinArr - 设置了最小百分比宽度(min-width="20%")的列 + const { scaleList, scaleMinList } = columnStore + + // 1. 首先处理设置了最小像素宽度的列 + // 这些列的宽度不能小于设定的最小宽度 + pxMinList.forEach((column) => { + const minWidth = parseInt(column.minWidth) tableWidth += minWidth column.renderWidth = minWidth }) - // 最小百分比 - let meanWidth = remainWidth / 100 - scaleMinArr.forEach((column) => { - let scaleWidth = Math.floor(parseInt(column.minWidth) * meanWidth) + // 计算1%宽度对应的像素值,用于处理百分比宽度 + const meanWidth = remainWidth / 100 + // 2. 处理设置了最小百分比宽度的列 + // 将百分比转换为实际像素值 + scaleMinList.forEach((column) => { + const scaleWidth = Math.floor(parseInt(column.minWidth) * meanWidth) tableWidth += scaleWidth column.renderWidth = scaleWidth }) - // 固定百分比 - scaleArr.forEach((column) => { - let scaleWidth = Math.floor(parseInt(column.width) * meanWidth) + // 3. 处理设置了固定百分比宽度的列 + scaleList.forEach((column) => { + const scaleWidth = Math.floor(parseInt(column.width) * meanWidth) tableWidth += scaleWidth column.renderWidth = scaleWidth }) - // 固定宽 - pxArr.forEach((column) => { - let width = parseInt(column.width) + // 4. 处理设置了固定像素宽度的列 + pxList.forEach((column) => { + const width = parseInt(column.width) tableWidth += width column.renderWidth = width }) - // 调整了列宽 - resizeArr.forEach((column) => { - let width = parseInt(column.resizeWidth) + // 5. 最后处理用户手动调整过宽度的列 + // 这些列的宽度优先级最高 + resizeList.forEach((column) => { + const width = parseInt(column.resizeWidth) tableWidth += width column.renderWidth = width }) + // 返回计算后的表格总宽度和平均宽度 + // tableWidth用于后续的自适应计算 + // meanWidth用于计算剩余空间的分配 return { tableWidth, meanWidth } } -export const calcTableWidth = ({ bodyWidth, columnStore, fit, minCellWidth, remainWidth }) => { - let { tableWidth, meanWidth } = initTableWidth({ remainWidth, columnStore }) - let { pxMinList: pxMinArr, scaleMinList: scaleMinArr, autoList: autoArr } = columnStore +/** + * 计算表格的总宽度,并处理列的自适应和填充逻辑 + * @param bodyWidth - 表格容器的宽度 + * @param columnStore - 列配置存储对象 + * @param fit - 是否需要填充满容器 + * @param minCellWidth - 最小单元格宽度 + * @returns {number} 返回计算后的表格总宽度 + */ +export const calcTableWidth = ({ bodyWidth, columnStore, fit, minCellWidth }) => { + // 初始化表格宽度和平均宽度 + let { tableWidth, meanWidth } = initTableWidth({ remainWidth: bodyWidth, columnStore }) + // 获取最小像素宽度列、最小百分比宽度列和自适应列 + let { pxMinList, scaleMinList, autoList } = columnStore - remainWidth -= tableWidth - meanWidth = remainWidth > 0 ? Math.floor(remainWidth / (scaleMinArr.length + pxMinArr.length + autoArr.length)) : 0 + // 计算剩余可用宽度 + const remainWidth = bodyWidth - tableWidth + // 计算每列平均可分配宽度 + // 如果有剩余宽度,则平均分配给最小宽度列和自适应列 + // 如果没有剩余宽度,则为0 + meanWidth = remainWidth > 0 ? Math.floor(remainWidth / (scaleMinList.length + pxMinList.length + autoList.length)) : 0 + // 如果需要填充满容器 if (fit) { + // 如果有剩余宽度 if (remainWidth > 0) { - scaleMinArr.concat(pxMinArr).forEach((column) => { + // 将剩余宽度平均分配给最小百分比宽度列和最小像素宽度列 + scaleMinList.concat(pxMinList).forEach((column) => { tableWidth += meanWidth column.renderWidth += meanWidth }) } } else { + // 如果不需要填充满容器,则使用最小单元格宽度 meanWidth = minCellWidth } - // 自适应修补一些列的宽度 - tableWidth = adaptive({ autoArr, meanWidth, minCellWidth, tableWidth, fit, bodyWidth }) + // 处理自适应列的宽度 + tableWidth = adaptive({ autoList, meanWidth, minCellWidth, tableWidth, fit, bodyWidth }) + + // 计算处理完自适应列后与容器的剩余空间 const remainingSpace = bodyWidth - tableWidth - // 如果还有空间剩余 + + // 如果需要填充满容器且还有剩余空间 if (fit && remainingSpace > 0) { - scaleMinArr - .concat(pxMinArr) + // 将剩余空间以1px为单位分配给最小百分比宽度列和最小像素宽度列 + // 分配数量不超过剩余空间大小 + scaleMinList + .concat(pxMinList) .slice(0, remainingSpace) .forEach((column) => { tableWidth += 1 @@ -125,6 +152,14 @@ export const calcTableWidth = ({ bodyWidth, columnStore, fit, minCellWidth, rema return tableWidth } +/** + * 设置固定列的左侧或右侧定位位置 + * @param columnList - 需要处理的列数组 + * @param direction - 方向,'left' 或 'right' + * @param headerEl - 表头DOM元素 + * @param bodyEl - 表体DOM元素 + * @param scrollbarWidth - 滚动条宽度 + */ const setLeftOrRightPosition = ({ columnList, direction, headerEl, bodyEl, scrollbarWidth }) => { // 这里需要浅拷贝一份,避免改变原始数据的顺序 const colList = columnList.slice() @@ -156,7 +191,12 @@ const setLeftOrRightPosition = ({ columnList, direction, headerEl, bodyEl, scrol }, 0) } -// 设置分组父表头冻结列sticky布局的left和right值 +/** + * 设置分组表头中固定列的定位位置 + * 处理多级表头情况下的固定列定位 + * @param columnChart - 列层级关系图 + * @param direction - 方向,'left' 或 'right' + */ const setGroupHeaderPosition = ({ columnChart, direction }) => { // 这里需要浅拷贝一份,避免改变原始数据的顺序 const colChart = columnChart.slice() @@ -191,7 +231,13 @@ const setGroupHeaderPosition = ({ columnChart, direction }) => { }) } -// 设置分组父表头冻结列是否左侧最后一项,或者是否右侧第一项 +/** + * 标记分组表头中的特殊固定列 + * 设置左侧最后一个固定列和右侧第一个固定列的标记 + * @param columnChart - 列层级关系图 + * @param leftList - 左侧固定列数组 + * @param rightList - 右侧固定列数组 + */ const setGroupHeaderLastOrFirst = ({ columnChart, leftList, rightList }) => { columnChart.forEach((columns) => { const len = columns.length @@ -207,6 +253,16 @@ const setGroupHeaderLastOrFirst = ({ columnChart, leftList, rightList }) => { }) } +/** + * 计算并设置固定列的粘性定位位置 + * 主要处理固定列的位置计算和样式设置 + * @param headerEl - 表头DOM元素 + * @param bodyEl - 表体DOM元素 + * @param columnStore - 列配置存储对象 + * @param scrollbarWidth - 滚动条宽度 + * @param columnChart - 列层级关系图 + * @param isGroup - 是否是分组表头 + */ export const calcFixedStickyPosition = ({ headerEl, bodyEl, columnStore, scrollbarWidth, columnChart, isGroup }) => { // 获取左侧和右侧冻结列 const { leftList, rightList } = columnStore diff --git a/packages/vue/src/grid/src/table/src/utils/computeScrollLoad.ts b/packages/vue/src/grid/src/table/src/utils/computeScrollLoad.ts deleted file mode 100644 index 3f06d598a0..0000000000 --- a/packages/vue/src/grid/src/table/src/utils/computeScrollLoad.ts +++ /dev/null @@ -1,100 +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 { toNumber } from '@opentiny/vue-renderless/grid/static/' -import { browserInfo } from '@opentiny/utils' - -let isWebkit = browserInfo['-webkit'] - -export function computeScrollYLoad({ _vm, scrollLoad, scrollY, scrollYLoad, scrollYStore, tableBodyElem }) { - if (scrollYLoad || scrollLoad) { - // 获取表格体默认第一行的高度 - scrollYStore.rowHeight = _vm.getRowHeight() - } - - if (scrollYLoad) { - // scrollY.vSize用户配置的可视区域渲染行数 - const bodyHeight = toNumber( - tableBodyElem.style?.height || tableBodyElem.style?.maxHeight || tableBodyElem.clientHeight - ) - let visibleYSize = toNumber(scrollY.vSize || Math.ceil(bodyHeight / scrollYStore.rowHeight)) - - scrollYStore.visibleSize = visibleYSize - - // 自动优化 - if (!scrollY.oSize) { - scrollYStore.offsetSize = visibleYSize - } - - // scrollY.rSize用户配置的每次渲染行数 - if (!scrollY.rSize) { - // 如果是webkit内核浏览器则渲染行数+2,性能差的浏览器就*2,防止滚动时出现较多白屏 - scrollYStore.renderSize = visibleYSize + (isWebkit ? 2 : visibleYSize) - } - - // 计算需要渲染的表格数据,并更新YSpace元素高度用来显示正确的滚动条长度 - _vm.updateScrollYData() - } else { - _vm.updateScrollYSpace() - } -} - -export function computeScrollXLoad({ _vm, scrollX, scrollXLoad, scrollXStore, tableBodyElem, visibleColumn }) { - if (scrollXLoad) { - /** - * 使用 “列渲染宽度累加方式” 优化默认 visibleSize 的计算, - * 旧的使用总宽度除以第一列宽度方式,会出现 visibleSize 很大出现渲染空白问题。 - */ - const clientWidth = tableBodyElem.clientWidth - let width = 0 - let visibleXSize = 0 - const len = visibleColumn.length - const colsWidth = visibleColumn?.map((i) => i.renderWidth).sort((a, b) => a - b) || [] - for (let i = 0; i < len; i++) { - width += colsWidth[i] - // 当虚拟滚动可见列宽度大于表格宽度或者循环结束,保存可见列大小 - if (width > clientWidth || i === len - 1) { - visibleXSize = i + 1 - break - } - } - - visibleXSize = toNumber(scrollX.vSize || visibleXSize) - - scrollXStore.visibleSize = visibleXSize - // 自动优化 - if (!scrollX.oSize) { - scrollXStore.offsetSize = visibleXSize - } - - if (!scrollX.rSize) { - scrollXStore.renderSize = visibleXSize + 2 - } - - // 处理x轴虚拟滚动渲染数据 - _vm.updateScrollXData() - } else { - _vm.updateScrollXSpace() - } -} diff --git a/packages/vue/src/grid/src/table/src/utils/handleCacheData.ts b/packages/vue/src/grid/src/table/src/utils/handleCacheData.ts new file mode 100644 index 0000000000..df4872a569 --- /dev/null +++ b/packages/vue/src/grid/src/table/src/utils/handleCacheData.ts @@ -0,0 +1,51 @@ +// 创建快速缓存 +export const buildCache = (tableData, { treeConfig, treeOrdered }) => { + // 使用WeakMap存储原始数据和备份数据的映射关系,WeakMap的key是对象的弱引用,可以被垃圾回收 + const backupMap = new WeakMap() + // 从treeConfig中解构children和temporaryIndex配置,temporaryIndex默认为'_$index_' + const { children, temporaryIndex = '_$index_' } = treeConfig || {} + // 判断是否为树形结构且未排序 + const isTreeOrderedFalse = treeConfig && !treeOrdered + + /** + * 递归遍历数组,为每个节点创建备份 + * @param {Array} arr - 要遍历的数组 + * @param {number} rowLevel - 当前行的层级 + * @param {string} parentIndex - 父节点的索引 + * @returns {Array} 备份数据数组 + */ + const traverse = (arr, rowLevel, parentIndex) => { + // 存储当前层级所有节点的备份数据 + const backup = [] + + if (Array.isArray(arr) && arr.length > 0) { + arr.forEach((row, rowIndex) => { + // 如果是未排序的树形结构,生成临时索引,格式为:父索引.当前索引 + if (isTreeOrderedFalse) { + row[temporaryIndex] = `${parentIndex ? `${parentIndex}.` : ''}${rowIndex + 1}` + } + + // 深拷贝当前行数据,children置为null避免循环引用 + const backupRow = structuredClone({ ...row, [children]: null }) + + // 将备份数据加入备份数组 + backup.push(backupRow) + // 建立原始数据和备份数据的映射关系 + backupMap.set(row, backupRow) + + // 如果存在子节点,递归处理子节点 + if (row[children]) { + backupRow[children] = traverse(row[children], rowLevel + 1, isTreeOrderedFalse ? row[temporaryIndex] : '') + } + }) + } + + return backup + } + + // 从根节点开始遍历,生成备份数据 + const backupData = traverse(tableData, 0, '') + + // 返回备份数据和映射关系 + return { backupData, backupMap } +} diff --git a/packages/vue/src/grid/src/table/src/utils/handleGlobalKeydownEvent.ts b/packages/vue/src/grid/src/table/src/utils/handleGlobalKeydownEvent.ts index 6cda2ec595..571758ef3c 100644 --- a/packages/vue/src/grid/src/table/src/utils/handleGlobalKeydownEvent.ts +++ b/packages/vue/src/grid/src/table/src/utils/handleGlobalKeydownEvent.ts @@ -1,26 +1,11 @@ /** - * 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. - * + * 规则1:处理ESC键按下事件 + * @param {Object} params - 参数对象 + * @param {boolean} params.isKeyEsc - 是否按下ESC键 + * @param {Object} params._vm - Vue实例 + * @param {Event} params.event - 事件对象 + * @param {Object} params.actived - 当前激活的单元格信息 + * @param {Object} params.mouseConfig - 鼠标配置 */ function rule1({ isKeyEsc, _vm, event, actived, mouseConfig }) { return { @@ -29,6 +14,15 @@ function rule1({ isKeyEsc, _vm, event, actived, mouseConfig }) { } } +/** + * 规则2:处理空格键按下事件(用于选择复选框或单选框) + * @param {Object} params - 参数对象 + * @param {boolean} params.isKeySpacebar - 是否按下空格键 + * @param {Object} params.keyboardConfig - 键盘配置 + * @param {Object} params.selected - 当前选中的单元格信息 + * @param {Object} params._vm - Vue实例 + * @param {Event} params.event - 事件对象 + */ function rule2({ isKeySpacebar, keyboardConfig, selected, _vm, event }) { return { match: () => @@ -41,6 +35,19 @@ function rule2({ isKeySpacebar, keyboardConfig, selected, _vm, event }) { } } +/** + * 规则3:处理回车键按下事件 + * @param {Object} params - 参数对象 + * @param {boolean} params.isKeyEnter - 是否按下回车键 + * @param {Object} params.keyboardConfig - 键盘配置 + * @param {Object} params.selected - 当前选中的单元格信息 + * @param {Object} params.actived - 当前激活的单元格信息 + * @param {Object} params.treeConfig - 树形配置 + * @param {boolean} params.highlightCurrentRow - 是否高亮当前行 + * @param {Object} params.currentRow - 当前行信息 + * @param {Object} params._vm - Vue实例 + * @param {Event} params.event - 事件对象 + */ function rule3(args) { const { isKeyEnter, keyboardConfig, selected, actived } = args const { treeConfig, highlightCurrentRow, currentRow, _vm, event } = args @@ -54,6 +61,13 @@ function rule3(args) { } } +/** + * 规则4:处理右键菜单操作 + * @param {Object} params - 参数对象 + * @param {boolean} params.isOperCtxMenu - 是否操作右键菜单 + * @param {Object} params._vm - Vue实例 + * @param {Event} params.event - 事件对象 + */ function rule4({ isOperCtxMenu, _vm, event }) { return { match: () => isOperCtxMenu, @@ -61,6 +75,14 @@ function rule4({ isOperCtxMenu, _vm, event }) { } } +/** + * 规则5:处理F2键按下事件(用于编辑单元格) + * @param {Object} params - 参数对象 + * @param {boolean} params.isKeyF2 - 是否按下F2键 + * @param {Object} params._vm - Vue实例 + * @param {Event} params.event - 事件对象 + * @param {Object} params.selected - 当前选中的单元格信息 + */ function rule5({ isKeyF2, _vm, event, selected }) { return { match: () => isKeyF2, @@ -68,6 +90,15 @@ function rule5({ isKeyF2, _vm, event, selected }) { } } +/** + * 规则6:处理方向键按下事件 + * @param {Object} params - 参数对象 + * @param {boolean} params.isOperArrowKeys - 是否按下方向键 + * @param {Object} params.keyboardConfig - 键盘配置 + * @param {Object} params._vm - Vue实例 + * @param {Event} params.event - 事件对象 + * @param {Object} params.selected - 当前选中的单元格信息 + */ function rule6({ isOperArrowKeys, keyboardConfig, _vm, event, selected }) { return { match: () => isOperArrowKeys && keyboardConfig.isArrow, @@ -75,6 +106,16 @@ function rule6({ isOperArrowKeys, keyboardConfig, _vm, event, selected }) { } } +/** + * 规则7:处理Tab键按下事件 + * @param {Object} params - 参数对象 + * @param {boolean} params.isKeyTab - 是否按下Tab键 + * @param {Object} params.keyboardConfig - 键盘配置 + * @param {Object} params._vm - Vue实例 + * @param {Event} params.event - 事件对象 + * @param {Object} params.selected - 当前选中的单元格信息 + * @param {Object} params.actived - 当前激活的单元格信息 + */ function rule7({ isKeyTab, keyboardConfig, _vm, event, selected, actived }) { return { match: () => isKeyTab && keyboardConfig.isTab, @@ -82,6 +123,19 @@ function rule7({ isKeyTab, keyboardConfig, _vm, event, selected, actived }) { } } +/** + * 规则8:处理删除键和退格键按下事件 + * @param {Object} params - 参数对象 + * @param {boolean} params.isKeyDel - 是否按下删除键 + * @param {Object} params.treeConfig - 树形配置 + * @param {boolean} params.highlightCurrentRow - 是否高亮当前行 + * @param {Object} params.currentRow - 当前行信息 + * @param {boolean} params.isKeyBack - 是否按下退格键 + * @param {Object} params.keyboardConfig - 键盘配置 + * @param {Object} params._vm - Vue实例 + * @param {Event} params.event - 事件对象 + * @param {Object} params.selected - 当前选中的单元格信息 + */ function rule8(args) { const { isKeyDel, treeConfig, highlightCurrentRow, currentRow } = args const { isKeyBack, keyboardConfig, _vm, event, selected } = args @@ -93,6 +147,18 @@ function rule8(args) { } } +/** + * 规则9:处理复制粘贴相关快捷键(Ctrl+A/X/C/V) + * @param {Object} params - 参数对象 + * @param {Object} params.keyboardConfig - 键盘配置 + * @param {boolean} params.isKeyWithCtrl - 是否按下Ctrl键 + * @param {boolean} params.isKeyA - 是否按下A键 + * @param {boolean} params.isKeyX - 是否按下X键 + * @param {boolean} params.isKeyC - 是否按下C键 + * @param {boolean} params.isKeyV - 是否按下V键 + * @param {Object} params._vm - Vue实例 + * @param {Event} params.event - 事件对象 + */ function rule9({ keyboardConfig, isKeyWithCtrl, isKeyA, isKeyX, isKeyC, isKeyV, _vm, event }) { return { match: () => keyboardConfig.isCut && isKeyWithCtrl && (isKeyA || isKeyX || isKeyC || isKeyV), @@ -100,6 +166,16 @@ function rule9({ keyboardConfig, isKeyWithCtrl, isKeyA, isKeyX, isKeyC, isKeyV, } } +/** + * 规则10:处理其他按键按下事件(用于直接编辑) + * @param {Object} params - 参数对象 + * @param {Object} params.keyboardConfig - 键盘配置 + * @param {boolean} params.isKeyWithCtrl - 是否按下Ctrl键 + * @param {Object} params._vm - Vue实例 + * @param {Event} params.event - 事件对象 + * @param {Object} params.selected - 当前选中的单元格信息 + * @param {Object} params.actived - 当前激活的单元格信息 + */ function rule10({ keyboardConfig, isKeyWithCtrl, _vm, event, selected, actived }) { // 如果同一个单元格已经被激活编辑态,那么就不重复触发 return { @@ -108,10 +184,19 @@ function rule10({ keyboardConfig, isKeyWithCtrl, _vm, event, selected, actived } action: () => _vm.handleOtherKeyDown({ event, selected }) } } + +/** + * 全局键盘按下事件处理函数 + * @param {Event} event - 键盘事件对象 + * @param {Object} _vm - Vue实例 + */ export function onGlobalKeydown(event, _vm) { + // 获取配置信息 let { isCtxMenu, ctxMenuStore, mouseConfig = {}, keyboardConfig = {} } = _vm let { treeConfig, highlightCurrentRow, currentRow } = _vm let { selected, actived } = _vm.editStore + + // 获取按键信息 let eventKeyCode = event.keyCode let isKeyWithCtrl = event.ctrlKey let isKeyF2 = eventKeyCode === 113 @@ -131,27 +216,32 @@ export function onGlobalKeydown(event, _vm) { let isKeyBack = eventKeyCode === 8 let isOperArrowKeys = isKeyLeftArrow || isKeyUpArrow || isKeyRightArrow || isKeyDwArrow let isOperCtxMenu = isCtxMenu && ctxMenuStore.visible && (isKeyEnter || isKeySpacebar || isOperArrowKeys) + + // 定义所有键盘事件处理规则 let rules = [ + // ESC键处理 rule1({ isKeyEsc, _vm, event, actived, mouseConfig }), - // 空格键支持选中复选列 + // 空格键处理(用于选择复选框/单选框) rule2({ isKeySpacebar, keyboardConfig, selected, _vm, event }), - // 如果是激活状态,退则出到下一行 + // 回车键处理 rule3({ isKeyEnter, keyboardConfig, selected, actived, treeConfig, highlightCurrentRow, currentRow, _vm, event }), - // 如果配置了右键菜单; 支持方向键操作、回车 + // 右键菜单处理 rule4({ isOperCtxMenu, _vm, event }), - // 如果按下了 F2 键 + // F2键处理 rule5({ isKeyF2, _vm, event, selected }), - // 如果按下了方向键 + // 方向键处理 rule6({ isOperArrowKeys, keyboardConfig, _vm, event, selected }), - // 如果按下了 Tab 键切换 + // Tab键处理 rule7({ isKeyTab, keyboardConfig, _vm, event, selected, actived }), - // 如果是删除键 + // 删除键和退格键处理 rule8({ isKeyDel, treeConfig, highlightCurrentRow, currentRow, isKeyBack, keyboardConfig, _vm, event, selected }), + // 复制粘贴快捷键处理 rule9({ keyboardConfig, isKeyWithCtrl, isKeyA, isKeyX, isKeyC, isKeyV, _vm, event }), - // 如果是按下非功能键之外允许直接编辑 + // 其他按键处理(用于直接编辑) rule10({ keyboardConfig, isKeyWithCtrl, _vm, event, selected, actived }) ] + // 遍历规则,执行匹配的规则动作 for (let i = 0; i < rules.length; i++) { if (rules[i].match()) { return rules[i].action() diff --git a/packages/vue/src/grid/src/table/src/utils/handleGlobalMousedownEvent.ts b/packages/vue/src/grid/src/table/src/utils/handleGlobalMousedownEvent.ts index 0e7ef17463..dc02deebb6 100644 --- a/packages/vue/src/grid/src/table/src/utils/handleGlobalMousedownEvent.ts +++ b/packages/vue/src/grid/src/table/src/utils/handleGlobalMousedownEvent.ts @@ -1,53 +1,59 @@ /** - * 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. - * + * 处理全局鼠标按下事件在筛选器包装器上的逻辑 + * @param {Object} params - 参数对象 + * @param {HTMLElement} params.$el - 表格根元素 + * @param {Object} params._vm - Vue实例 + * @param {Event} params.event - 鼠标事件对象 + * @param {Object} params.filterStore - 筛选器状态存储对象 + * @param {Object} params.filterWrapper - 筛选器包装器组件实例 */ export function handleGlobalMousedownOnFilterWrapper({ $el, _vm, event, filterStore, filterWrapper }) { if (filterWrapper) { if (_vm.getEventTargetNode(event, $el, 'tiny-grid-filter-wrapper').flag) { - // 如果点击了筛选按钮 + // 如果点击了筛选按钮,不做任何处理 } else if (_vm.getEventTargetNode(event, filterWrapper.$el).flag) { - // 如果点击筛选容器 + // 如果点击筛选容器,不做任何处理 } else if (_vm.getEventTargetNode(event, document.body, 'tiny-popper').flag) { - // 如果点击筛选容器下拉弹出的弹窗 + // 如果点击筛选容器下拉弹出的弹窗,不做任何处理 } else { + // 如果点击了其他区域,关闭筛选器并触发清除筛选事件 _vm.closeFilter() _vm.preventEvent(event, 'event.clear_filter', filterStore.args, _vm.closeFilter) } } } +/** + * 处理全局鼠标按下事件在右键菜单上的逻辑 + * @param {Object} params - 参数对象 + * @param {Object} params._vm - Vue实例 + * @param {Object} params.ctxMenuStore - 右键菜单状态存储对象 + * @param {Event} params.event - 鼠标事件对象 + */ export function handleGlobalMousedownOnCtxMenu({ _vm, ctxMenuStore, event }) { + // 如果右键菜单可见,且点击的不是右键菜单包装器内部,则关闭菜单 if (ctxMenuStore.visible && _vm.$refs.ctxWrapper && !_vm.getEventTargetNode(event, _vm.$refs.ctxWrapper.$el).flag) { _vm.closeMenu() } } +/** + * 处理全局失焦事件 + * @param {Object} params - 参数对象 + * @param {Object} params._vm - Vue实例 + * @param {Object} params.actived - 当前激活的单元格信息 + * @param {Event} params.event - 事件对象 + * @returns {boolean} 返回是否处理了失焦事件 + */ export function handleGlobalBlurOutside({ _vm, actived, event }) { - // false->退出编辑clearActived; undefined->逻辑不变化; true->保留编辑状态,直接return + // 调用自定义的失焦处理函数,返回值: + // false -> 退出编辑状态并清除激活状态 + // undefined -> 不改变当前逻辑 + // true -> 保留编辑状态 let custblur = _vm.blurOutside(actived, event) if (typeof custblur === 'boolean') { + // 如果返回false,则延迟清除激活状态 custblur || setTimeout(() => _vm.clearActived(event)) return true } @@ -55,29 +61,55 @@ export function handleGlobalBlurOutside({ _vm, actived, event }) { return false } +/** + * 判断是否需要清除激活状态 + * @param {Object} params - 参数对象 + * @param {HTMLElement} params.$el - 表格根元素 + * @param {Object} params._vm - Vue实例 + * @param {Object} params.actived - 当前激活的单元格信息 + * @param {Object} params.editConfig - 编辑配置 + * @param {Event} params.event - 事件对象 + * @param {boolean} params.isClear - 是否清除状态 + * @param {boolean} params.isReadonlyCol - 是否为只读列 + * @returns {boolean} 返回是否需要清除激活状态 + */ export function handleGlobalIsClear({ $el, _vm, actived, editConfig, event, isClear, isReadonlyCol }) { if (editConfig.mode === 'row') { + // 获取点击的行节点 let rowNode = _vm.getEventTargetNode(event, $el, 'tiny-grid-body__row') + // 判断是否点击了不同的行 let isOtherRow = rowNode.flag ? rowNode.targetElem !== actived.args.cell.parentNode : 0 if (editConfig.trigger === 'manual') { - // manual 触发,如果点击了不同行 + // 手动触发模式下,如果点击了不同行且不是激活行,则清除状态 isClear = !_vm.getEventTargetNode(event, $el, 'row__actived').flag && isOtherRow } else { - // click,dblclick 触发,如果点击了不同行的非编辑列 + // 点击或双击触发模式下,如果点击了不同行的非编辑列,则清除状态 isClear = isOtherRow && isReadonlyCol } } else { - // cell 方式,如果是非编辑列 + // 单元格编辑模式下,如果是非编辑列,则清除状态 isClear = isReadonlyCol } return isClear } +/** + * 处理清除激活状态的逻辑 + * @param {Object} params - 参数对象 + * @param {HTMLElement} params.$el - 表格根元素 + * @param {Object} params._vm - Vue实例 + * @param {Event} params.event - 事件对象 + * @param {boolean} params.isClear - 是否清除状态 + */ export function handleGlobalClearActived({ $el, _vm, event, isClear }) { const tableContent = _vm.$refs.tableBody?.$refs.table - // 如果点击了当前表格之外 + // 在以下情况下清除激活状态: + // 1. 需要清除状态 + // 2. 点击了表格外部 + // 3. 点击了表头 + // 4. 点击了表格内容区域外部 if ( isClear || !_vm.getEventTargetNode(event, $el).flag || diff --git a/packages/vue/src/grid/src/table/src/utils/handleOtherKeyDown.ts b/packages/vue/src/grid/src/table/src/utils/handleOtherKeyDown.ts index 15608e6f1a..5d53510c64 100644 --- a/packages/vue/src/grid/src/table/src/utils/handleOtherKeyDown.ts +++ b/packages/vue/src/grid/src/table/src/utils/handleOtherKeyDown.ts @@ -1,27 +1,3 @@ -/** - * 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. - * - */ export function checkOtherKey(keyCode) { return ( (keyCode >= 48 && keyCode <= 57) || diff --git a/packages/vue/src/grid/src/table/src/utils/handleResolveColumn.ts b/packages/vue/src/grid/src/table/src/utils/handleResolveColumn.ts deleted file mode 100644 index 49049f87d2..0000000000 --- a/packages/vue/src/grid/src/table/src/utils/handleResolveColumn.ts +++ /dev/null @@ -1,97 +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 { get } from '@opentiny/vue-renderless/grid/static/' - -export function mapFetchColumnPromise({ _vm, fetchColumns, tableColumn }) { - return fetchColumns.map(({ format, columnValues }) => - format.async.fetch({ columns: tableColumn, columnValues, $table: _vm }) - ) -} - -export function preprocessDataObjectFormat({ columnCount, columnValues, columnValuesMap, fields }) { - if (columnCount) { - columnValues.forEach((col) => { - if (typeof col === 'object') { - const label = get(col, fields.text || 'label') - const value = get(col, fields.value || 'value') - - col.label = label - columnValuesMap[value] = col - } - }) - } -} - -export function preventDupRender({ - asyncColumnName, - cellTexts, - cellValuesCount, - columnData, - columnValuesMap, - isRender, - property, - renderCount, - row, - splitConfig -}) { - let cellEachIndex = 0 - - if (!isRender && cellValuesCount) { - let cellLabel - let cellValues = [row[property]] - - // 默认不开启 - if (splitConfig.enabled === true) { - cellValues = (row[property] || '').split(splitConfig.valueSplit || ',') - } - - // 拼接单元格内容 - while (cellEachIndex < cellValuesCount) { - const activeValue = cellValues[cellEachIndex] - const currentRow = columnValuesMap[activeValue] - - cellLabel = typeof currentRow === 'object' ? currentRow.label : currentRow - cellTexts.push(cellLabel) - columnData.push({ - label: cellLabel, - value: cellValues[cellEachIndex], - row: currentRow - }) - - cellEachIndex++ - renderCount++ - } - - row[asyncColumnName] = cellTexts.join(splitConfig.textSplit || ',') - } - - return renderCount -} - -export function handleResolveColumnComplete({ _vm, columnData, complete }) { - if (typeof complete === 'function') { - complete({ columnData, $table: _vm }) - } -} diff --git a/packages/vue/src/grid/src/table/src/utils/refreshColumn.ts b/packages/vue/src/grid/src/table/src/utils/refreshColumn.ts index 97f8f375af..4bf6692a41 100644 --- a/packages/vue/src/grid/src/table/src/utils/refreshColumn.ts +++ b/packages/vue/src/grid/src/table/src/utils/refreshColumn.ts @@ -1,36 +1,20 @@ -/** - * 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 { findTree, eachTree, toNumber } from '@opentiny/vue-renderless/grid/static/' +import { findTree, eachTree, toNumber } from '../../../utils/static/' import { error, warn } from '../../../tools' +/** + * 处理表头分组显示的逻辑 + * @param {Object} params - 参数对象 + * @param {Object} params._vm - Vue实例 + * @param {Boolean} params.isGroup - 是否为分组表头 + * @param {Object} params.headerProps - 表头属性 + */ export function onGroupHeader({ _vm, isGroup, headerProps }) { if (isGroup) { eachTree( _vm.collectColumn, (column) => { if (column.children && column.children.length) { + // 如果列有子列,则根据子列的可见性决定父列的可见性 column.visible = !!findTree( column.children, (subColumn) => (subColumn.children && subColumn.children.length ? 0 : subColumn.visible), @@ -43,12 +27,28 @@ export function onGroupHeader({ _vm, isGroup, headerProps }) { } } +/** + * 重新分配非固定列的列表 + * @param {Object} params - 参数对象 + * @param {Array} params.centerList - 中间列列表 + * @param {Object} params.column - 当前列对象 + */ export function reassignNotFixed({ centerList, column }) { if (!column.fixed) { centerList.push(column) } } +/** + * 重新分配右侧固定列的列表 + * @param {Object} params - 参数对象 + * @param {Object} params.column - 当前列对象 + * @param {Number} params.columnIndex - 列索引 + * @param {Boolean} params.isColspan - 是否跨列 + * @param {Number} params.rightEndIndex - 右侧结束索引 + * @param {Array} params.rightList - 右侧列列表 + * @returns {Object} 返回更新后的跨列状态和右侧结束索引 + */ export function reassignFixedRight({ column, columnIndex, isColspan, rightEndIndex, rightList }) { if (column.fixed === 'right') { if (!isColspan) { @@ -69,6 +69,17 @@ export function reassignFixedRight({ column, columnIndex, isColspan, rightEndInd return { isColspan, rightEndIndex } } +/** + * 重新分配左侧固定列的列表 + * @param {Object} params - 参数对象 + * @param {Object} params.column - 当前列对象 + * @param {Number} params.columnIndex - 列索引 + * @param {Boolean} params.isColspan - 是否跨列 + * @param {Array} params.leftList - 左侧列列表 + * @param {Number} params.leftStartIndex - 左侧开始索引 + * @param {Number} params.letIndex - 左侧索引 + * @returns {Object} 返回更新后的左侧开始索引、左侧索引和跨列状态 + */ export function reassignFixedLeft({ column, columnIndex, isColspan, leftList, leftStartIndex, letIndex }) { if (column.fixed === 'left') { if (leftStartIndex === null) { @@ -89,18 +100,40 @@ export function reassignFixedLeft({ column, columnIndex, isColspan, leftList, le return { leftStartIndex, letIndex, isColspan } } +/** + * 显示分组固定列的错误信息 + * @param {Object} params - 参数对象 + * @param {Boolean} params.isColspan - 是否跨列 + * @param {Boolean} params.isGroup - 是否为分组表头 + * @param {Number} params.leftStartIndex - 左侧开始索引 + * @param {Number} params.rightEndIndex - 右侧结束索引 + * @param {Array} params.visibleColumn - 可见列数组 + */ export function showGroupFixedError({ isColspan, isGroup, leftStartIndex, rightEndIndex, visibleColumn }) { if (isGroup && (isColspan || leftStartIndex || (rightEndIndex !== null && rightEndIndex !== visibleColumn.length))) { error('ui.grid.error.groupFixed') } } +/** + * 处理横向滚动加载的逻辑 + * @param {Object} params - 参数对象 + * @param {Object} params._vm - Vue实例 + * @param {Object} params.scrollX - 横向滚动配置 + * @param {Boolean} params.scrollXLoad - 是否启用横向滚动加载 + * @param {Object} params.scrollXStore - 横向滚动状态存储 + * @param {Array} params.tableColumn - 表格列数组 + * @param {Array} params.visibleColumn - 可见列数组 + * @returns {Array} 返回处理后的表格列数组 + */ export function onScrollXLoad({ _vm, scrollX, scrollXLoad, scrollXStore, tableColumn, visibleColumn }) { if (scrollXLoad) { + // 检查是否启用了可调整大小功能 if (_vm.resizable || visibleColumn.some((column) => column.resizable)) { warn('ui.grid.error.notResizable') } + // 更新滚动状态 Object.assign(scrollXStore, { startIndex: 0, visibleIndex: 0, @@ -108,6 +141,7 @@ export function onScrollXLoad({ _vm, scrollX, scrollXLoad, scrollXStore, tableCo offsetSize: toNumber(scrollX.oSize) }) + // 根据滚动状态截取需要显示的列 tableColumn = visibleColumn.slice(scrollXStore.startIndex, scrollXStore.startIndex + scrollXStore.renderSize) } diff --git a/packages/vue/src/grid/src/table/src/utils/triggerCellClickEvent.ts b/packages/vue/src/grid/src/table/src/utils/triggerCellClickEvent.ts index 1f1a7ba6a0..a9589b24bb 100644 --- a/packages/vue/src/grid/src/table/src/utils/triggerCellClickEvent.ts +++ b/packages/vue/src/grid/src/table/src/utils/triggerCellClickEvent.ts @@ -1,26 +1,10 @@ /** - * 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. - * + * 判断目标元素是否为单选框或复选框 + * @param {Event} event - 事件对象 + * @param {Object} column - 列配置对象 + * @param {string} colType - 列类型 + * @param {string} targetType - 目标类型 + * @returns {boolean} 如果目标元素是单选框或复选框则返回true */ export function isTargetRadioOrCheckbox(event, column, colType, targetType) { const target = event.target @@ -33,6 +17,16 @@ export function isTargetRadioOrCheckbox(event, column, colType, targetType) { ) } +/** + * 处理展开列点击事件 + * @param {Object} params - 参数对象 + * @param {HTMLElement} params.$el - 表格元素 + * @param {Object} params._vm - Vue实例 + * @param {Object} params.column - 列配置对象 + * @param {Event} params.event - 事件对象 + * @param {Object} params.expandConfig - 展开配置 + * @param {Object} params.params - 其他参数 + */ export function onClickExpandColumn({ $el, _vm, column, event, expandConfig, params }) { if ( (expandConfig.trigger === 'row' || (column.type === 'expand' && expandConfig.trigger === 'cell')) && @@ -42,12 +36,31 @@ export function onClickExpandColumn({ $el, _vm, column, event, expandConfig, par } } +/** + * 处理树形节点列点击事件 + * @param {Object} params - 参数对象 + * @param {Object} params._vm - Vue实例 + * @param {Object} params.column - 列配置对象 + * @param {Event} params.event - 事件对象 + * @param {Object} params.params - 其他参数 + * @param {Object} params.treeConfig - 树形配置 + */ export function onClickTreeNodeColumn({ _vm, column, event, params, treeConfig }) { if (treeConfig.trigger === 'row' || (column.treeNode && treeConfig.trigger === 'cell')) { _vm.triggerTreeExpandEvent(event, params) } } +/** + * 处理当前行高亮点击事件 + * @param {Object} params - 参数对象 + * @param {HTMLElement} params.$el - 表格元素 + * @param {Object} params._vm - Vue实例 + * @param {Event} params.event - 事件对象 + * @param {boolean} params.highlightCurrentRow - 是否高亮当前行 + * @param {Object} params.params - 其他参数 + * @param {Object} params.radioConfig - 单选框配置 + */ export function onHighlightCurrentRow({ $el, _vm, event, highlightCurrentRow, params, radioConfig }) { if (highlightCurrentRow) { if ( @@ -60,6 +73,16 @@ export function onHighlightCurrentRow({ $el, _vm, event, highlightCurrentRow, pa } } +/** + * 处理单选框列点击事件 + * @param {Object} params - 参数对象 + * @param {HTMLElement} params.$el - 表格元素 + * @param {Object} params._vm - Vue实例 + * @param {Object} params.column - 列配置对象 + * @param {Event} params.event - 事件对象 + * @param {Object} params.params - 其他参数 + * @param {Object} params.radioConfig - 单选框配置 + */ export function onClickRadioColumn({ $el, _vm, column, event, params, radioConfig }) { if ( (radioConfig.trigger === 'row' || (column.type === 'radio' && radioConfig.trigger === 'cell')) && @@ -69,6 +92,15 @@ export function onClickRadioColumn({ $el, _vm, column, event, params, radioConfi } } +/** + * 处理选择列点击事件 + * @param {Object} params - 参数对象 + * @param {Object} params._vm - Vue实例 + * @param {Object} params.column - 列配置对象 + * @param {Event} params.event - 事件对象 + * @param {Object} params.params - 其他参数 + * @param {Object} params.selectConfig - 选择配置 + */ export function onClickSelectColumn({ _vm, column, event, params, selectConfig }) { if ( (selectConfig.trigger === 'row' || (column.type === 'selection' && selectConfig.trigger === 'cell')) && @@ -78,6 +110,19 @@ export function onClickSelectColumn({ _vm, column, event, params, selectConfig } } } +/** + * 处理单元格选择点击事件 + * @param {Object} params - 参数对象 + * @param {Object} params._vm - Vue实例 + * @param {Object} params.actived - 当前激活的单元格信息 + * @param {HTMLElement} params.cell - 单元格元素 + * @param {Object} params.column - 列配置对象 + * @param {Object} params.editConfig - 编辑配置 + * @param {Event} params.event - 事件对象 + * @param {Object} params.mouseConfig - 鼠标配置 + * @param {Object} params.params - 其他参数 + * @param {Object} params.row - 行数据 + */ export function onClickCellSelect({ _vm, actived, cell, column, editConfig, event, mouseConfig, params, row }) { if (!mouseConfig.checked && editConfig) { if (editConfig.trigger === 'manual') { diff --git a/packages/vue/src/grid/src/table/src/utils/updateStyle.ts b/packages/vue/src/grid/src/table/src/utils/updateStyle.ts index f9d9e513a0..3e44d24beb 100644 --- a/packages/vue/src/grid/src/table/src/utils/updateStyle.ts +++ b/packages/vue/src/grid/src/table/src/utils/updateStyle.ts @@ -1,30 +1,13 @@ +import { arrayEach, toNumber } from '../../../utils/static/' +import { isScale } from '../../../utils/utils' + /** - * 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. - * + * 设置表格元素的宽度 + * @param {Object} params - 参数对象 + * @param {number} params.scrollbarWidth - 滚动条宽度 + * @param {number} params.tWidth - 表格宽度 + * @param {HTMLElement} params.tableElem - 表格元素 */ -import { arrayEach, toNumber } from '@opentiny/vue-renderless/grid/static/' -import { isScale } from '@opentiny/vue-renderless/grid/utils' - function setTableElemWidth({ scrollbarWidth, tWidth, tableElem }) { if (tableElem && tWidth === null) { tableElem.style.width = tWidth @@ -35,6 +18,14 @@ function setTableElemWidth({ scrollbarWidth, tWidth, tableElem }) { } } +/** + * 获取表格宽度 + * @param {Object} params - 参数对象 + * @param {boolean} params.scrollXLoad - 是否启用横向滚动加载 + * @param {number} params.tWidth - 表格宽度 + * @param {Array} params.tableColumn - 表格列配置 + * @returns {Object} 返回表格列配置和宽度 + */ function getTableWidth({ scrollXLoad, tWidth, tableColumn }) { if (scrollXLoad) { tWidth = tableColumn.reduce((previous, column) => previous + column.renderWidth, 0) @@ -43,6 +34,24 @@ function getTableWidth({ scrollXLoad, tWidth, tableColumn }) { return { tableColumn, tWidth } } +/** + * 布局表尾 + * @param {Object} params - 参数对象 + * @param {Object} params.elemStore - 元素存储对象 + * @param {number} params.customHeight - 自定义高度 + * @param {number} params.footerHeight - 表尾高度 + * @param {number} params.headerHeight - 表头高度 + * @param {boolean} params.scrollXLoad - 是否启用横向滚动加载 + * @param {number} params.scrollbarWidth - 滚动条宽度 + * @param {HTMLElement} params.tableElem - 表格元素 + * @param {number} params.scrollbarHeight - 滚动条高度 + * @param {Array} params.tableColumn - 表格列配置 + * @param {number} params.tableHeight - 表格高度 + * @param {number} params.tableWidth - 表格宽度 + * @param {HTMLElement} params.wrapperElem - 包装元素 + * @param {HTMLElement} params.fixedWrapperElem - 固定列包装元素 + * @returns {Array} 返回表格列配置 + */ function layoutFooter({ elemStore, customHeight, @@ -84,7 +93,14 @@ function layoutFooter({ return tableColumn } -// 计算colgroup元素中每个col元素的width属性,保证表头和表格体保持对齐 +/** + * 计算colgroup元素中每个col元素的width属性,保证表头和表格体保持对齐 + * @param {Object} params - 参数对象 + * @param {Object} params.elemStore - 元素存储对象 + * @param {Object} params.fullColumnIdData - 完整的列ID数据 + * @param {string} params.layout - 布局类型 + * @param {number} params.scrollbarWidth - 滚动条宽度 + */ function layoutColgroup({ elemStore, fullColumnIdData, layout, scrollbarWidth }) { let colgroupElem = elemStore[`main-${layout}-colgroup`] let colElemHandler = (colElem) => { @@ -105,6 +121,18 @@ function layoutColgroup({ elemStore, fullColumnIdData, layout, scrollbarWidth }) } } +/** + * 布局表头 + * @param {Object} params - 参数对象 + * @param {Object} params.elemStore - 元素存储对象 + * @param {string} params.layout - 布局类型 + * @param {boolean} params.scrollXLoad - 是否启用横向滚动加载 + * @param {number} params.scrollbarWidth - 滚动条宽度 + * @param {Array} params.tableColumn - 表格列配置 + * @param {HTMLElement} params.tableElem - 表格元素 + * @param {number} params.tableWidth - 表格宽度 + * @returns {Array} 返回表格列配置 + */ function layoutHeader({ elemStore, layout, scrollXLoad, scrollbarWidth, tableColumn, tableElem, tableWidth }) { let tWidth = tableWidth let repairElem = elemStore[`main-${layout}-repair`] @@ -122,12 +150,30 @@ function layoutHeader({ elemStore, layout, scrollXLoad, scrollbarWidth, tableCol return tableColumn } +/** + * 布局表格 + * @param {Object} params - 参数对象 + * @param {number} params.tWidth - 表格宽度 + * @param {HTMLElement} params.tableElem - 表格元素 + */ function layoutTable({ tWidth, tableElem }) { if (tableElem) { tableElem.style.width = tWidth ? `${tWidth}px` : tWidth } } +/** + * 布局表格体包装器 + * @param {Object} params - 参数对象 + * @param {number} params.footerHeight - 表尾高度 + * @param {number} params.customHeight - 自定义高度 + * @param {number} params.headerHeight - 表头高度 + * @param {number} params.maxHeight - 最大高度 + * @param {number} params.minHeight - 最小高度 + * @param {number} params.parentHeight - 父元素高度 + * @param {HTMLElement} params.wrapperElem - 包装元素 + * @returns {Object} 返回最大高度和最小高度 + */ function layoutBodyWrapper({ footerHeight, customHeight, @@ -135,27 +181,38 @@ function layoutBodyWrapper({ maxHeight, minHeight, parentHeight, - wrapperElem, - scrollbarWidth + wrapperElem }) { if (wrapperElem) { + // 如果设置了自定义高度 if (customHeight > 0) { + // 计算内容区域高度 = 自定义高度 - 表头高度 - 表尾高度 const contentHeight = customHeight - headerHeight - footerHeight + // 设置包装器元素的固定高度 wrapperElem.style.height = `${contentHeight}px` } + // 如果设置了最大高度 if (maxHeight) { + // 如果最大高度是百分比,则根据父元素高度计算实际像素值 + // 否则转换为数字 maxHeight = isScale(maxHeight) ? Math.floor((parseInt(maxHeight) / 100) * parentHeight) : toNumber(maxHeight) + // 计算内容区域最大高度 = 最大高度 - 表头高度 - 表尾高度 const contentHeight = maxHeight - headerHeight - footerHeight + // 设置包装器元素的最大高度 wrapperElem.style.maxHeight = `${contentHeight}px` } + // 如果设置了最小高度 if (minHeight) { + // 如果最小高度是百分比,则根据父元素高度计算实际像素值 + // 否则转换为数字 minHeight = isScale(minHeight) ? Math.floor((parseInt(minHeight) / 100) * parentHeight) : toNumber(minHeight) + // 设置包装器元素的最小高度 = 最小高度 - 表头高度 - 表尾高度 wrapperElem.style.minHeight = `${minHeight - headerHeight - footerHeight}px` } } @@ -163,18 +220,35 @@ function layoutBodyWrapper({ return { maxHeight, minHeight } } +/** + * 布局空数据块 + * @param {Object} params - 参数对象 + * @param {HTMLElement} params.emptyBlockElem - 空数据块元素 + * @param {number} params.tWidth - 表格宽度 + */ function layoutEmptyBlock({ emptyBlockElem, tWidth }) { if (emptyBlockElem) { emptyBlockElem.style.width = tWidth ? `${tWidth}px` : tWidth || '' } } +/** + * 布局表格体 + * @param {Object} options - 配置对象 + * @returns {Object} 返回最大高度、最小高度和表格列配置 + */ function layoutBody(options) { + // 从options中解构出布局相关的参数 let { customHeight, elemStore, footerHeight, headerHeight, layout } = options + // 从options中解构出高度和滚动相关的参数 let { maxHeight, minHeight, parentHeight, scrollXLoad } = options - let { scrollbarWidth, tableColumn, tableElem, tableWidth, wrapperElem } = options + // 从options中解构出表格元素相关的参数 + let { tableColumn, tableElem, tableWidth, wrapperElem } = options + // 获取空数据块元素 let emptyBlockElem = elemStore[`main-${layout}-emptyBlock`] + // 调用layoutBodyWrapper处理表格主体包装器的布局 + // 包括设置高度、最大/最小高度等 let ret = layoutBodyWrapper({ customHeight, footerHeight, @@ -182,50 +256,85 @@ function layoutBody(options) { maxHeight, minHeight, parentHeight, - scrollbarWidth, wrapperElem }) + // 获取处理后的最大和最小高度 maxHeight = ret.maxHeight minHeight = ret.minHeight + // 保存表格宽度 let tWidth = tableWidth - // 如果是固定列与设置了超出隐藏 + // 处理表格宽度 + // 如果启用了横向虚拟滚动,会根据实际情况调整表格和列的宽度 ret = getTableWidth({ scrollXLoad, tWidth, tableColumn }) - tableColumn = ret.tableColumn - tWidth = ret.tWidth + tableColumn = ret.tableColumn // 更新列配置 + tWidth = ret.tWidth // 更新表格宽度 + + // 设置表格元素的宽度 layoutTable({ tWidth, tableElem }) + + // 设置空数据块的宽度 layoutEmptyBlock({ emptyBlockElem, tWidth }) + + // 返回处理后的高度和列配置信息 return { maxHeight, minHeight, tableColumn } } +/** + * 处理布局 + * @param {Object} params - 参数对象 + * @returns {Object} 返回表格列配置、最大高度和最小高度 + */ export function handleLayout(params) { + // 从参数中解构出所需的配置项 let { - _vm, - columnStore, - customHeight, - fixedColumn, - fixedWrapperElem, - layout, - maxHeight, - minHeight, - parentHeight, - tableColumn + _vm, // Vue实例 + columnStore, // 列存储对象 + customHeight, // 自定义高度 + fixedColumn, // 固定列配置 + fixedWrapperElem, // 固定列包装器元素 + layout, // 布局类型(header/body/footer) + maxHeight, // 最大高度 + minHeight, // 最小高度 + parentHeight, // 父容器高度 + tableColumn // 表格列配置 } = params - let { elemStore, footerHeight, fullColumnIdData, headerHeight, showFooter } = _vm - let { overflowX, overflowY, scrollXLoad, scrollbarHeight, scrollbarWidth } = _vm - let { showOverflow: allColumnOverflow, tableHeight, tableWidth } = _vm + // 从Vue实例中解构出所需的属性和方法 + let { + elemStore, // 元素存储对象 + footerHeight, // 表尾高度 + fullColumnIdData, // 完整列ID数据 + headerHeight, // 表头高度 + showFooter // 是否显示表尾 + } = _vm + let { + overflowX, // 横向溢出处理方式 + overflowY, // 纵向溢出处理方式 + scrollXLoad, // 是否启用横向虚拟滚动 + scrollbarHeight, // 滚动条高度 + scrollbarWidth // 滚动条宽度 + } = _vm + let { + showOverflow: allColumnOverflow, // 所有列的溢出显示方式 + tableHeight, // 表格高度 + tableWidth // 表格宽度 + } = _vm + + // 获取主布局的包装器和表格元素 let wrapperElem = elemStore[`main-${layout}-wrapper`] let tableElem = elemStore[`main-${layout}-table`] + /* - * 表头体样式处理 - * 横向滚动渲染 + * 根据不同的布局类型(header/body/footer)进行相应的样式处理 */ if (layout === 'header') { + // 处理表头布局,主要处理横向虚拟滚动 tableColumn = layoutHeader({ elemStore, layout, scrollXLoad, scrollbarWidth, tableColumn, tableElem, tableWidth }) } else if (layout === 'body') { + // 处理表格主体布局,包括固定列、滚动条、溢出等 let ret = layoutBody({ ...{ allColumnOverflow, columnStore, customHeight, elemStore, fixedColumn, fixedWrapperElem }, ...{ footerHeight, headerHeight, layout, maxHeight, minHeight, overflowX, overflowY }, @@ -243,10 +352,12 @@ export function handleLayout(params) { } }) + // 更新最大/最小高度和列配置 maxHeight = ret.maxHeight minHeight = ret.minHeight tableColumn = ret.tableColumn } else if (layout === 'footer') { + // 处理表尾布局 tableColumn = layoutFooter({ elemStore, customHeight, @@ -263,8 +374,10 @@ export function handleLayout(params) { wrapperElem }) } - // 计算colgroup元素中每个col元素的width属性,保证表头和表格体保持对齐 + + // 处理colgroup,确保表头和表格体的列宽对齐 layoutColgroup({ elemStore, fullColumnIdData, layout, scrollbarWidth }) + // 返回处理后的列配置和高度信息 return { tableColumn, maxHeight, minHeight } } diff --git a/packages/vue/src/grid/src/toolbar/src/methods.ts b/packages/vue/src/grid/src/toolbar/src/methods.ts index b61cb6381d..1528929289 100644 --- a/packages/vue/src/grid/src/toolbar/src/methods.ts +++ b/packages/vue/src/grid/src/toolbar/src/methods.ts @@ -3,7 +3,6 @@ import { error } from '../../tools' import Modal from '@opentiny/vue-modal' import GlobalConfig from '../../config' import { emitEvent } from '@opentiny/vue-renderless/grid/utils' -import { h, hooks } from '@opentiny/vue-common' import { extend } from '@opentiny/utils' export function setBodyRecords({ body, insertRecords, pendingRecords }) { @@ -71,25 +70,6 @@ export function invokeSaveDataApi({ _vm, args, body, code, removeRecords, resolv } export default { - // 表格工具栏渲染器 - getRenderedToolbar({ $slots, _vm, loading, tableLoading, toolbar }) { - return (_vm.renderedToolbar = (() => { - let res = null - - if ($slots.toolbar) { - res = $slots.toolbar() - } else if (toolbar) { - res = h(hooks.toRaw(toolbar.component), { - ref: 'toolbar', - props: { loading: loading || tableLoading, ...toolbar }, - class: _vm.viewCls('toolbar'), - scopedSlots: toolbar.slots || {} - }) - } - - return res - })()) - }, handleSave(code, args) { let { saveData, isMsg } = this @@ -171,40 +151,54 @@ export default { handleFullScreen([show]) { this.fullScreenClass = show ? 'tiny-fullscreen-full' : '' this.$nextTick(() => { - this.recalculate(true) - emitEvent(this, 'fullscreen', show) - this.emitter.emit('fullscreen', show) + emitEvent(this, 'fullscreen', show) // 触发正常vue监听的事件比如@fullscreen + this.emitter.emit('fullscreen', show) // 触发配置式监听的事件 }) }, commitProxy(code, ...args) { + // 从按钮配置中获取对应code的方法 let btnMethod = Buttons.get(code) + // 根据不同的操作码执行相应的操作 if (code === 'insert') { + // 插入新行 this.insert() } else if (code === 'insert_actived') { + // 插入新行并激活该行 this.insert().then(({ row }) => this.setActiveRow(row)) } else if (code === 'mark_cancel') { + // 触发待处理事件 this.triggerPendingEvent(code) } else if (code === 'delete_selection') { + // 删除选中行,删除后会调用delete接口 this.handleDeleteRow(code, 'ui.grid.deleteSelectRecord', () => this.commitProxy(['delete', ...args])) } else if (code === 'remove_selection') { + // 移除选中行,仅从界面移除不调用接口 this.handleDeleteRow(code, 'ui.grid.removeSelectRecord', () => this.removeSelecteds()) } else if (code === 'export') { + // 导出CSV文件 this.exportCsv() } else if (code === 'reset_custom') { + // 重置表格配置 this.resetAll() - } else if (~['reload', 'query', 'prefetch'].indexOf(code)) { + } else if (['reload', 'query', 'prefetch'].includes(code)) { + // 处理数据刷新相关的操作 this.handleFetch(code, args) } else if (code === 'delete') { + // 删除数据 this.handleDelete(code, args) } else if (code === 'save') { + // 保存数据 this.handleSave() } else if (code === 'fullscreen') { + // 切换全屏显示 this.handleFullScreen(args) } else if (btnMethod) { + // 如果是自定义按钮方法,则调用对应的方法 btnMethod.call(this, { code, $grid: this }, ...args) } + // 等待DOM更新完成后返回Promise return this.$nextTick() }, handleDeleteRow(code, i18nKey, callback) { diff --git a/packages/vue/src/grid/src/tools/index.ts b/packages/vue/src/grid/src/tools/index.ts index d332269ade..fd4fcb6a93 100644 --- a/packages/vue/src/grid/src/tools/index.ts +++ b/packages/vue/src/grid/src/tools/index.ts @@ -1,29 +1,5 @@ -/** - * 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 { isString } from '@opentiny/vue-renderless/grid/static/' -import { getCellValue } from '@opentiny/vue-renderless/grid/utils' +import { isString } from '../utils/static/' +import { getCellValue } from '../utils/utils' import Formatter from './formatter' import GlobalConfig from '../config' diff --git a/packages/vue/src/grid/src/tools/logger.ts b/packages/vue/src/grid/src/tools/logger.ts index 0d7d92fe8a..f40792925e 100644 --- a/packages/vue/src/grid/src/tools/logger.ts +++ b/packages/vue/src/grid/src/tools/logger.ts @@ -1,17 +1,19 @@ import { logger } from '@opentiny/utils' import GlobalConfig from '../config' -const outLog = (type) => (message, detail) => { - let msg = `[tiny-grid] ${GlobalConfig.i18n(message) || message}` +const outLog = + (type: string) => + (message: string, detail?: string): string => { + let msg = `[tiny-grid] ${GlobalConfig.i18n(message) || message}` - if (detail) { - msg += `: ${detail}` - } + if (detail) { + msg += `: ${detail}` + } - logger[type](msg) + logger[type](msg) - return msg -} + return msg + } export const warn = outLog('warn') diff --git a/packages/vue/src/grid/src/tooltip/src/methods.ts b/packages/vue/src/grid/src/tooltip/src/methods.ts index bbf673648b..c09eb2ff53 100644 --- a/packages/vue/src/grid/src/tooltip/src/methods.ts +++ b/packages/vue/src/grid/src/tooltip/src/methods.ts @@ -37,9 +37,10 @@ export default { handleTooltip(event, column, row, showTip, isHeader) { // 当title配置为函数时,文本不会包裹tiny-grid-cell-text类名 const cell = - isHeader && !(typeof column.title === 'function') + this.hoverCell || + (isHeader && !(typeof column.title === 'function') ? event.currentTarget.querySelector('.tiny-grid-cell-text') - : event.currentTarget.querySelector('.tiny-grid-cell') + : event.currentTarget.querySelector('.tiny-grid-cell')) // 当用户悬浮在排序或者筛选图标按钮时不应该显示tooltip,使用头部插槽且文本超长时也应该显示 if (isHeader && event.target !== cell && !cell?.contains(event.target)) { diff --git a/packages/vue/src/grid/src/tree/src/methods.ts b/packages/vue/src/grid/src/tree/src/methods.ts index d6f1825257..7c8ffbccb5 100644 --- a/packages/vue/src/grid/src/tree/src/methods.ts +++ b/packages/vue/src/grid/src/tree/src/methods.ts @@ -71,7 +71,7 @@ export default { setTreeScrollYCache(this) - return this.$nextTick().then(this.recalculate) + return this.$nextTick() }, // 设置展开树形节点,二个参数设置这一行展开与否:支持单行,支持多行 setTreeExpansion(rows, expanded) { @@ -79,7 +79,7 @@ export default { let { accordion, children } = treeConfig let isToggle = arguments.length === 1 if (!rows) { - return this.$nextTick().then(this.recalculate) + return this.$nextTick() } if (!isArray(rows)) { rows = [rows] @@ -112,7 +112,7 @@ export default { setTreeScrollYCache(this) - return this.$nextTick().then(this.recalculate) + return this.$nextTick() }, hasTreeExpand(row) { return ~this.treeExpandeds.indexOf(row) @@ -121,7 +121,7 @@ export default { const hasExpand = this.treeExpandeds.length this.treeExpandeds = [] setTreeScrollYCache(this) - return this.$nextTick().then(() => (hasExpand ? this.recalculate() : 0)) + return this.$nextTick() }, getTreeExpandeds() { return this.treeExpandeds diff --git a/packages/vue/src/grid/src/utils/core/index.ts b/packages/vue/src/grid/src/utils/core/index.ts new file mode 100644 index 0000000000..8baa3b0ef9 --- /dev/null +++ b/packages/vue/src/grid/src/utils/core/index.ts @@ -0,0 +1,16 @@ +/** + * 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 StoreMap from './storeMap' +import Interceptor from './interceptor' + +export { StoreMap, Interceptor } diff --git a/packages/vue/src/grid/src/utils/core/interceptor.ts b/packages/vue/src/grid/src/utils/core/interceptor.ts new file mode 100644 index 0000000000..68f991d999 --- /dev/null +++ b/packages/vue/src/grid/src/utils/core/interceptor.ts @@ -0,0 +1,72 @@ +/** + * 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 { toString, each, remove } from '../static' + +const toType = (type) => toString(type).replace('_', '').toLowerCase() +const eventTypes = [ + 'created', + 'mounted', + 'activated', + 'beforeDestroy', + 'destroyed', + 'event.clearActived', + 'event.clearFilter', + 'event.showMenu', + 'event.keydown' +].map(toType) +const _storeMap = {} + +const Interceptor = { + mixin(map) { + each(map, (callback, type) => Interceptor.add(type, callback)) + return Interceptor + }, + get(type) { + return _storeMap[toType(type)] || [] + }, + add(type, callback) { + const selfType = toType(type) + + if (callback && ~eventTypes.indexOf(selfType)) { + _storeMap[selfType] = _storeMap[selfType] ? _storeMap[selfType] : [] + + _storeMap[selfType].push(callback) + } + + return Interceptor + }, + delete(type, callback) { + const eList = _storeMap[toType(type)] + + if (eList) { + remove(eList, (cb) => cb === callback) + } + + return Interceptor + } +} + +export default Interceptor diff --git a/packages/vue/src/grid/src/utils/core/storeMap.ts b/packages/vue/src/grid/src/utils/core/storeMap.ts new file mode 100644 index 0000000000..c34fded489 --- /dev/null +++ b/packages/vue/src/grid/src/utils/core/storeMap.ts @@ -0,0 +1,47 @@ +/** + * 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. + * + */ + +// 全局的快捷菜单 +const _storeMap = {} + +const storeMap = { + mixin(map) { + Object.assign(_storeMap, map) + return storeMap + }, + get(type) { + return _storeMap[type] + }, + add(type, callback) { + _storeMap[type] = callback + return storeMap + }, + delete(type) { + delete _storeMap[type] + return storeMap + } +} + +export default storeMap diff --git a/packages/vue/src/grid/src/utils/plugins/export.ts b/packages/vue/src/grid/src/utils/plugins/export.ts new file mode 100644 index 0000000000..847dca099b --- /dev/null +++ b/packages/vue/src/grid/src/utils/plugins/export.ts @@ -0,0 +1,202 @@ +/** + * 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 { xss } from '@opentiny/utils' +import { browserInfo } from '@opentiny/utils' +import { toTreeArray } from '../static' +import { getCellValue, getFuncText } from '../utils' +import { exportExcel } from './exportExcel' + +const getCsvLabelData = (columns, oData, tableElem) => { + const trElemList = tableElem.querySelectorAll('.tiny-grid__body-wrapper.body__wrapper .tiny-grid-body__row') + + const trData = [] + + for (let i = 0, len = trElemList.length; i < len; i++) { + const item = {} + const trElem = trElemList[i] + + columns.forEach((column) => { + const cell = trElem.querySelector(`.${column.id}`) + item[column.id] = cell ? cell.innerText.trim() : '' + }) + + trData.push(item) + } + + return trData +} + +const getCsvData = (opts, oData, oColumns, tableElem) => { + const isOriginal = opts.original + let columns = opts.columns ? opts.columns : oColumns + + if (opts.columnFilterMethod) { + columns = columns.filter(opts.columnFilterMethod) + } + + let datas = opts.data ? opts.data : isOriginal ? oData : getCsvLabelData(columns, oData, tableElem) + + if (opts.dataFilterMethod) { + datas = datas.filter(opts.dataFilterMethod) + } + + return { columns, datas } +} + +const getCsvContent = ($table, opts, oColumns, oData) => { + const isOriginal = opts.original + const tableEl = $table.$el + const tab = opts.useTabs === false ? '' : '\t' + const { columns, datas } = getCsvData(opts, oData, oColumns, tableEl) + let content = '\uFEFF' // BOM字节序标记 + const transfrom = (str) => { + if (typeof str === 'string' && str.replace(/ /g, '').match(/[\s,"]/)) { + str = '"' + str.replace(/"/g, '""') + '"' + } + + // 在 =、@、+、-开头的数据加前置空格,解决csv文件注入问题 + if (typeof str === 'string' && str.match(/^([@=]|([-\\+].*[^0-9\\.])).*$/)) { + str = ' ' + str + } + + return str + tab + } + + if (opts.isHeader) { + content += columns.map(({ own }) => transfrom(getFuncText(own.title || own.label))).join(',') + '\n' + } + + datas.forEach((row, rowIndex) => { + if (isOriginal) { + content += + columns + .map((column) => { + if (column.type === 'index') { + return column.indexMethod ? column.indexMethod(rowIndex) : rowIndex + 1 + } + + return transfrom(getCellValue(row, column)) + }) + .join(',') + '\n' + } else { + content += columns.map((column) => transfrom(row[column.id])).join(',') + '\n' + } + }) + + if (opts.isFooter) { + const footerData = $table.footerData + const footers = opts.footerFilterMethod ? footerData.filter(opts.footerFilterMethod) : footerData + const filterMaps = $table.tableColumn.map((column) => ~columns.indexOf(column)) + + footers.forEach((rows) => { + content += rows.filter((val, colIndex) => filterMaps[colIndex]).join(',') + '\n' + }) + } + + return content +} + +const getCsvUrl = (opts, content) => { + if (window.Blob && window.URL && window.URL.createObjectURL && browserInfo.name !== 'safari') { + return URL.createObjectURL(new Blob([content], { type: 'text/csv;charset=utf-8' })) + } + + return `data:attachment/csv;charset=utf-8,${encodeURIComponent(content)}` +} + +const downloadCsc = (options, content) => { + if (!options.download) { + return Promise.resolve(content) + } + + if (navigator.msSaveBlob && window.Blob) { + navigator.msSaveBlob(new Blob([content], { type: 'text/csv;charset=utf-8' }), options.filename) + } else { + const linkElem = document.createElement('a') + + linkElem.target = '_blank' + linkElem.rel = 'noopener noreferrer' + linkElem.download = options.filename + linkElem.href = xss.filterUrl(getCsvUrl(options, content)) + document.body.appendChild(linkElem) + linkElem.click() + document.body.removeChild(linkElem) + } +} + +/** + * 导出 csv 文件 + * 如果是启用了可视渲染,则只能导出数据源,可以配合 dataFilterMethod 函数自行转换数据 + * 如果是树表格,则默认是导出所有节点 + */ +export default { + _exportCsv(options) { + let { visibleColumn, scrollXLoad, scrollYLoad, treeConfig } = this + let mergedOpts = { + columns: null, + columnFilterMethod: (column) => column.property && !['index', 'selection', 'radio'].includes(column.type), + download: true, + data: null, + dataFilterMethod: null, + filename: 'table.csv', + footerFilterMethod: null, + isHeader: true, + isFooter: true, + original: !!treeConfig, + ...options + } + + if (!mergedOpts.filename.includes('.csv')) { + mergedOpts.filename += '.csv' + } + + if (!mergedOpts.original) { + if (scrollXLoad || scrollYLoad) { + mergedOpts.original = true + } + } + + let columns = visibleColumn + let oData = this.tableFullData + + if (treeConfig) { + oData = toTreeArray(oData, treeConfig) + } + + oData.forEach((value) => { + Object.keys(value).forEach((key) => { + if (value[key] === 0) { + value[key] = '0' + } + }) + }) + + return downloadCsc(mergedOpts, getCsvContent(this, mergedOpts, columns, oData)) + }, + _exportExcel(options) { + exportExcel(this, options) + } +} diff --git a/packages/vue/src/grid/src/utils/plugins/exportExcel.ts b/packages/vue/src/grid/src/utils/plugins/exportExcel.ts new file mode 100644 index 0000000000..dfc6afa42f --- /dev/null +++ b/packages/vue/src/grid/src/utils/plugins/exportExcel.ts @@ -0,0 +1,638 @@ +/* eslint-disable prefer-rest-params */ +/** + * 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 { extend } from '@opentiny/utils' +import { browserInfo } from '@opentiny/utils' + +const isIE = browserInfo.name === 'ie' +const rgbRegExp = /^rgba?\((\d+),\s(\d+),\s(\d+)([\s\S]*)\)$/ +const hexRegExp = /^#([a-zA-Z0-9]{3}|[a-zA-Z0-9]{6})$/ + +function computeColor(rgbColor) { + const rgb = rgbRegExp.exec(rgbColor).slice(1, 4) + return parseInt(rgb[0]).toString(16) + parseInt(rgb[1]).toString(16) + parseInt(rgb[2]).toString(16) +} + +function getBgc(dom) { + const getComputedStyle = window.getComputedStyle + + const backgroundColor = isIE ? dom.currentStyle.backgroundColor : getComputedStyle(dom).backgroundColor + + if (rgbRegExp.test(backgroundColor)) { + return computeColor(backgroundColor) + } else if (hexRegExp.test(backgroundColor)) { + const bgc = backgroundColor.slice(1) + + if (bgc.length === 3) { + return bgc + .split('') + .map((s) => s + s) + .join('') + } + + return bgc + } + + return 'ffffff' +} + +function funcFromCodePoint() { + let codeUnitArr = [] + let codeLength = 0 + let resultStr = '' + + for (let i = 0, length = arguments.length; i !== length; ++i) { + let cp = Number(arguments[i]) + + if (!(cp < 0x10ffff && cp >>> 0 === cp)) { + throw new RangeError('Invalid code point: ' + cp) + } + + if (cp <= 0xffff) { + codeLength = codeUnitArr.push(cp) + } else { + cp -= 0x10000 + codeLength = codeUnitArr.push((cp >> 10) + 0xd800, (cp % 0x400) + 0xdc00) + } + + if (codeLength >= 0x3fff) { + resultStr += String.fromCharCode.apply(null, codeUnitArr) + codeUnitArr.length = 0 + } + } + + return resultStr + String.fromCharCode.apply(null, codeUnitArr) +} + +const defaultOptions = { + plugins: { + XLSX: null, // xlsx 插件 + XLSXX: null, // xlsx-style 插件 + FileSaver: null // FileSaver 插件 + }, + table: { + sheetName: 'tiny-sheet', + formatMethod: ({ value, type }) => ({ value, type }), + styleMethod: ({ style }) => style, + rowHeightMethod: ({ height }) => height, + columnWidthMethod: ({ width }) => width, + mergesMethod: (merges) => merges + } +} + +function s2ab(s) { + const buf = new ArrayBuffer(s.length) + const view = new Uint8Array(buf) + + for (let i = 0; i !== s.length; i++) { + view[i] = s.charCodeAt(i) & 0xff + } + + return buf +} + +function adjustHeight(hpx) { + const n = parseInt(hpx / 36) + return hpx - 10 * n +} + +function initHeaderDatas({ datas, headerRowCount, headerTrs }) { + let maxColSpanCount = [-1, 0] + + for (let i = 0; i < headerRowCount; i++) { + const childNodes = headerTrs[i].childNodes + let colSpanCount = 0 + + for (let j = 0; j < childNodes.length; j++) { + if (~childNodes[j].className.split(/\s/).indexOf('col__gutter')) { + break + } + + colSpanCount += childNodes[j].colSpan + } + + if (colSpanCount > maxColSpanCount[1]) { + maxColSpanCount[0] = i + maxColSpanCount[1] = colSpanCount + } + } + + for (let i = 0; i < headerRowCount; i++) { + datas.push([]) + + for (let j = 0; j < maxColSpanCount[1]; j++) { + datas[i].push('') + } + } +} + +function buildHeaderMerges({ datas, headerTrs, rowHeightMethod, ths, ws }) { + for (let i = 0; i < headerTrs.length; i++) { + const headerTrRect = headerTrs[i].getBoundingClientRect() + const childNodes = headerTrs[i].childNodes + let rowHeight = Math.round(headerTrRect.height) + let lastColSpan = 0 + + rowHeight = rowHeightMethod ? rowHeightMethod({ rowIndex: i, height: rowHeight }) : rowHeight + ws['!rows'].push({ hpx: adjustHeight(rowHeight) }) + + for (let j = 0; j < childNodes.length; j++) { + if (~childNodes[j].className.split(/\s/).indexOf('col__gutter')) { + break + } + + ths.push(childNodes[j]) + + const colSpan = childNodes[j].colSpan + const rowSpan = childNodes[j].rowSpan + const textContent = childNodes[j].textContent + + if (colSpan > 1) { + ws['!merges'].push({ + s: { r: i, c: lastColSpan }, + e: { r: i, c: lastColSpan + colSpan - 1 } + }) + } + + if (rowSpan > 1) { + ws['!merges'].push({ + s: { r: i, c: lastColSpan }, + e: { r: i + rowSpan - 1, c: lastColSpan } + }) + } + + if (i !== headerTrs.length - 1) { + datas[i].splice(lastColSpan, 1, textContent) + } + + lastColSpan += colSpan + } + } +} + +function buildColids({ $table, colids, columnWidthMethod, datas, ths, ws }) { + const getColWidth = (column, level) => { + if (column.children) { + for (let i = 0; i < column.children.length; i++) { + getColWidth(column.children[i], level + 1) + } + } else { + const colid = column.id + + for (let i = 0; i < ths.length; i++) { + if (ths[i].dataset.colid === colid) { + colids.push(colid) + + const thRect = ths[i].getBoundingClientRect() + let columnWidth = Math.round(thRect.width) + + columnWidth = columnWidthMethod + ? columnWidthMethod({ + columnIndex: colids.length - 1, + width: columnWidth + }) + : columnWidth + ws['!cols'].push({ wch: Math.round(columnWidth / 10) }) + datas[level].splice( + colids.length - 1, + 1, + typeof column.title === 'string' ? column.title : ths[i].textContent + ) + + break + } + } + } + } + const collectColumn = $table.collectColumn + + for (let i = 0; i < collectColumn.length; i++) { + getColWidth(collectColumn[i], 0) + } +} + +function buildHeader({ $table, colids, datas, headerRowCount, headerTrs, opts, ws }) { + if (headerRowCount === 0) { + return + } + + const rowHeightMethod = opts.table.rowHeightMethod + const columnWidthMethod = opts.table.columnWidthMethod + const ths = [] + + initHeaderDatas({ datas, headerRowCount, headerTrs }) + + buildHeaderMerges({ datas, headerTrs, rowHeightMethod, ths, ws }) + + buildColids({ $table, colids, columnWidthMethod, datas, ths, ws }) +} + +function updateRowsDatas({ childNodes, colids, datas, i, offset, rowHeight, rowHeightMethod, ws }) { + rowHeight = rowHeightMethod ? rowHeightMethod({ rowIndex: i + offset, height: rowHeight }) : rowHeight + ws['!rows'].push({ hpx: adjustHeight(rowHeight) }) + datas.push([]) + + for (let j = 0; j < colids.length; j++) { + let rowTd = null + let textContent = '' + + for (let k = 0; k < childNodes.length; k++) { + if (childNodes[k].dataset.colid === colids[j]) { + rowTd = childNodes[k] + break + } + } + + if (rowTd) { + textContent = rowTd.textContent + rowTd = null + } + + datas[offset + i].push(textContent) + } +} + +function updateMerges({ childNodes, colids, i, offset, ws }) { + for (let j = 0; j < childNodes.length; j++) { + const colSpan = childNodes[j].colSpan + const rowSpan = childNodes[j].rowSpan + const colid = childNodes[j].dataset.colid + const sc = colids.indexOf(colid) + const sr = offset + i + + if (colSpan > 1 && rowSpan > 1) { + ws['!merges'].push({ + s: { r: sr, c: sc }, + e: { r: sr + rowSpan - 1, c: sc + colSpan - 1 } + }) + } else if (colSpan > 1) { + ws['!merges'].push({ + s: { r: sr, c: sc }, + e: { r: sr, c: sc + colSpan - 1 } + }) + } else if (rowSpan > 1) { + ws['!merges'].push({ + s: { r: sr, c: sc }, + e: { r: sr + rowSpan - 1, c: sc } + }) + } + } +} + +function buildBody({ bodyRowCount, bodyTrs, colids, datas, headerRowCount, opts, ws }) { + if (bodyRowCount === 0) { + return + } + + const rowHeightMethod = opts.table.rowHeightMethod + const offset = headerRowCount + + for (let i = 0; i < bodyTrs.length; i++) { + const bodyTrRect = bodyTrs[i].getBoundingClientRect() + const childNodes = bodyTrs[i].childNodes + let rowHeight = Math.round(bodyTrRect.height) + + updateRowsDatas({ + colids, + childNodes, + i, + datas, + rowHeight, + rowHeightMethod, + offset, + ws + }) + + updateMerges({ childNodes, colids, i, offset, ws }) + } +} + +function genExcelColNames() { + const excelColNames = [] + const f = funcFromCodePoint + + for (let i = -1; i < 26; i++) { + for (let j = 0; j < 26; j++) { + excelColNames.push((i === -1 ? '' : f(65 + i)) + f(65 + j)) + } + } + + return excelColNames +} + +function buildRef({ colids, datas, excelColNames, ws }) { + // fullref + let fullref = '' + + if (colids.length > 0 && datas.length > 0) { + fullref = 'A1:' + excelColNames[colids.length - 1] + datas.length + } + + ws['!fullref'] = fullref + ws['!ref'] = fullref +} + +function updateCellStyle({ + bodyRowCount, + bodyTrBgcArr, + footerTrBgc, + headerRowCount, + headerWrapperBgc, + rowIndex, + showBorder, + style +}) { + style.font = { name: 'Microsoft YaHei', sz: 12, color: { rgb: '000000' } } + + if (rowIndex < headerRowCount) { + style.fill = { fgColor: { rgb: headerWrapperBgc } } + + style.font.bold = true + } else if (rowIndex < headerRowCount + bodyRowCount) { + style.fill = { fgColor: { rgb: bodyTrBgcArr[rowIndex - headerRowCount] } } + } else { + style.fill = { fgColor: { rgb: footerTrBgc } } + } + + if (showBorder) { + style.border = { + top: { style: 'thin', color: { rgb: '000000' } }, + bottom: { style: 'thin', color: { rgb: '000000' } }, + left: { style: 'thin', color: { rgb: '000000' } }, + right: { style: 'thin', color: { rgb: '000000' } } + } + } + + style.alignment = { vertical: 'center', horizontal: 'left', wrapText: false } +} + +function buildDatas({ + showBorder, + bodyRowCount, + bodyTrBgcArr, + headerRowCount, + colids, + datas, + footerTrBgc, + headerWrapperBgc, + excelColNames, + opts, + $table, + ws +}) { + if (datas.length === 0) { + return + } + + const styleMethod = opts.table.styleMethod + const formatMethod = opts.table.formatMethod + const fullColumn = $table.getTableColumn().fullColumn + const isIndexColData = (i, j, indexColIndex, headerRowCount, bodyRowCount) => + indexColIndex === j && i >= headerRowCount && i < headerRowCount + bodyRowCount + let indexColIndex + + for (let i = 0; i < fullColumn.length; i++) { + if (fullColumn[i].type === 'index') { + indexColIndex = colids.indexOf(fullColumn[i].id) + break + } + } + + for (let i = 0; i < datas.length; i++) { + for (let j = 0; j < datas[i].length; j++) { + let type = isIndexColData(i, j, indexColIndex, headerRowCount, bodyRowCount) ? 'n' : 's' + let value = isIndexColData(i, j, indexColIndex, headerRowCount, bodyRowCount) + ? parseInt(datas[i][j]) + : datas[i][j].trim() + let style = {} + + updateCellStyle({ + bodyRowCount, + bodyTrBgcArr, + columnIndex: j, + footerTrBgc, + headerRowCount, + headerWrapperBgc, + rowIndex: i, + showBorder, + style + }) + + if (styleMethod) { + style = styleMethod({ rowIndex: i, columnIndex: j, style }) + } + + if (formatMethod) { + const ret = formatMethod({ rowIndex: i, columnIndex: j, value, type }) + type = ret.type + value = ret.value + } + + ws[excelColNames[j] + (i + 1)] = { t: type, v: value, s: style } + } + } +} + +function buildWb({ XLSXX, opts, ws }) { + ws = ws['!cols'].length === 0 || ws['!rows'].length === 0 ? {} : ws + + const sheetName = opts.table.sheetName + const merges = ws['!merges'] + + if (merges) { + const mergesMethod = opts.table.mergesMethod + ws['!merges'] = mergesMethod ? mergesMethod(merges) : merges + } + + const wb = { + Props: { + Application: 'SheetJS', + SheetNames: [sheetName], + Worksheets: 1 + }, + SSF: XLSXX.SSF.get_table(), + SheetNames: [sheetName], + Sheets: { + [sheetName]: ws + } + } + + return wb +} + +function computeBodyTrBgcArr(bodyTrs, isStripe) { + const arr = [] + + for (let i = 0; i < bodyTrs.length; i++) { + arr.push(isStripe ? getBgc(bodyTrs[i]) : 'ffffff') + } + + return arr +} + +function queryDom($table) { + const headerTrs = $table.$el.querySelectorAll('.tiny-grid__header-wrapper.body__wrapper .tiny-grid-header__row') + const bodyTrs = $table.$el.querySelectorAll('.tiny-grid__body-wrapper.body__wrapper .tiny-grid-body__row:not(.group)') + const footerTrs = $table.$el.querySelectorAll('.tiny-grid__footer-wrapper.body__wrapper .tiny-grid-footer__row') + const headerWrapper = $table.$el.querySelector('.tiny-grid__header-wrapper.body__wrapper') + + return { bodyTrs, footerTrs, headerTrs, headerWrapper } +} + +function getTableAttr($table, headerWrapper, bodyTrs) { + const showBorder = ~$table.$el.className.split(/\s/).indexOf('tiny-grid__border') + const isStripe = ~$table.$el.className.split(/\s/).indexOf('tiny-grid__stripe') + const headerWrapperBgc = headerWrapper ? getBgc(headerWrapper) : 'ffffff' + const bodyTrBgcArr = computeBodyTrBgcArr(bodyTrs, isStripe) + const footerTrBgc = 'ffffff' + + return { showBorder, headerWrapperBgc, bodyTrBgcArr, footerTrBgc } +} + +function buildColidsByVisibleColumn({ $table, bodyRowCount, bodyTrs, colids, opts, ws }) { + if (colids.length > 0 || bodyRowCount === 0) { + return + } + + const columnWidthMethod = opts.table.columnWidthMethod + const tds = [] + + for (let i = 0; i < bodyTrs.length; i++) { + for (let j = 0; j < bodyTrs[i].childNodes.length; j++) { + tds.push(bodyTrs[i].childNodes[j]) + } + } + + for (let i = 0; i < $table.visibleColumn.length; i++) { + let colid = $table.visibleColumn[i].id + + for (let j = 0; j < tds.length; j++) { + if (tds[j].dataset.colid === colid) { + colids.push(colid) + + const tdRect = tds[j].getBoundingClientRect() + const colSpan = tds[j].colSpan + let columnWidth = Math.round(tdRect.width / colSpan) + + columnWidth = columnWidthMethod + ? columnWidthMethod({ + columnIndex: colids.length - 1, + width: columnWidth + }) + : columnWidth + ws['!cols'].push({ wch: Math.round(columnWidth / 10) }) + + break + } + } + } +} + +function buildFooter({ bodyRowCount, colids, datas, footerRowCount, footerTrs, headerRowCount, opts, ws }) { + if (footerRowCount === 0) { + return + } + + const rowHeightMethod = opts.table.rowHeightMethod + const offset = headerRowCount + bodyRowCount + + for (let i = 0; i < footerTrs.length; i++) { + const footerTrRect = footerTrs[i].getBoundingClientRect() + const childNodes = footerTrs[i].childNodes + let rowHeight = Math.round(footerTrRect.height) + + updateRowsDatas({ + childNodes, + colids, + datas, + i, + offset, + rowHeight, + rowHeightMethod, + ws + }) + + updateMerges({ childNodes, colids, i, offset, ws }) + } +} + +function createExcelFromDom($table, opts) { + const { XLSXX } = opts.plugins + const { bodyTrs, footerTrs, headerTrs, headerWrapper } = queryDom($table) + const { showBorder, headerWrapperBgc, bodyTrBgcArr, footerTrBgc } = getTableAttr($table, headerWrapper, bodyTrs) + const headerRowCount = headerTrs.length + const bodyRowCount = bodyTrs.length + const footerRowCount = footerTrs.length + const ws = {} + const datas = [] + const colids = [] + const excelColNames = genExcelColNames() + + ws['!rows'] = [] + ws['!cols'] = [] + ws['!merges'] = [] + + buildHeader({ $table, colids, datas, headerRowCount, headerTrs, opts, ws }) + + buildColidsByVisibleColumn({ $table, bodyRowCount, bodyTrs, colids, opts, ws }) + + buildBody({ bodyRowCount, bodyTrs, colids, datas, headerRowCount, opts, ws }) + + buildFooter({ bodyRowCount, colids, datas, footerRowCount, footerTrs, headerRowCount, opts, ws }) + + buildRef({ colids, datas, excelColNames, ws }) + + buildDatas({ + $table, + bodyRowCount, + bodyTrBgcArr, + colids, + datas, + excelColNames, + footerTrBgc, + headerRowCount, + headerWrapperBgc, + opts, + showBorder, + ws + }) + + return buildWb({ XLSXX, opts, ws }) +} + +export function exportExcel($table, options) { + const opts = extend(true, {}, options) + + opts.plugins = opts.plugins || {} + + if (!opts.plugins.XLSX || !opts.plugins.XLSXX || !opts.plugins.FileSaver) { + return + } + + opts.table = opts.table || {} + + opts.table.sheetName = opts.table.sheetName || defaultOptions.table.sheetName + opts.table.formatMethod = opts.table.formatMethod || defaultOptions.table.formatMethod + opts.table.styleMethod = opts.table.styleMethod || defaultOptions.table.styleMethod + opts.table.rowHeightMethod = opts.table.rowHeightMethod || defaultOptions.table.rowHeightMethod + opts.table.columnWidthMethod = opts.table.columnWidthMethod || defaultOptions.table.columnWidthMethod + opts.table.mergesMethod = opts.table.mergesMethod || defaultOptions.table.mergesMethod + + const wb = createExcelFromDom($table, opts) + + const { XLSXX, FileSaver } = opts.plugins + const wopts = { bookType: 'xlsx', bookSST: false, type: 'binary' } + const wbout = XLSXX.write(wb, wopts) + + FileSaver.saveAs(new Blob([s2ab(wbout)], { type: '' }), opts.table.sheetName + '.xlsx') +} diff --git a/packages/vue/src/grid/src/utils/plugins/header.ts b/packages/vue/src/grid/src/utils/plugins/header.ts new file mode 100644 index 0000000000..317ae74265 --- /dev/null +++ b/packages/vue/src/grid/src/utils/plugins/header.ts @@ -0,0 +1,96 @@ +/** + * 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. + * + */ + +const columnIsVisible = (children) => + Array.isArray(children) && children.length && children.some((column) => column.visible) + +export const getAllColumns = (columns) => { + const result = [] + + columns.forEach((column) => { + if (column.visible) { + const children = column.children + + if (columnIsVisible(children)) { + result.push(column) + result.push.apply(result, getAllColumns(children)) + } else { + result.push(column) + } + } + }) + + return result +} + +export const convertToRows = (originColumns) => { + let maxLevel = 1 + const walkTree = (column, parent) => { + if (parent) { + column.level = parent.level + 1 + + if (maxLevel < column.level) { + maxLevel = column.level + } + } + + const children = column.children + + if (columnIsVisible(children)) { + let colSpan = 0 + + children.forEach((childColumn) => { + if (childColumn.visible) { + walkTree(childColumn, column) + colSpan += childColumn.colSpan + } + }) + column.colSpan = colSpan + } else { + column.colSpan = 1 + } + } + + originColumns.forEach((column) => { + column.level = 1 + walkTree(column) + }) + + const allRows = [] + + for (let i = 0; i < maxLevel; i++) { + allRows.push([]) + } + + const allColumns = getAllColumns(originColumns) + + allColumns.forEach((column) => { + column.rowSpan = columnIsVisible(column.children) ? 1 : maxLevel - column.level + 1 + + allRows[column.level - 1].push(column) + }) + + return allRows +} diff --git a/packages/vue/src/grid/src/utils/plugins/resize.ts b/packages/vue/src/grid/src/utils/plugins/resize.ts new file mode 100644 index 0000000000..bed96373ff --- /dev/null +++ b/packages/vue/src/grid/src/utils/plugins/resize.ts @@ -0,0 +1,99 @@ +/** + * 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 { browserInfo } from '@opentiny/utils' +import { remove } from '../static' + +let resizeTimeout +let defaultInterval = 250 +const eventStore = [] +let eventHandle + +const eventListener = () => { + clearTimeout(resizeTimeout) + resizeTimeout = setTimeout(eventHandle, defaultInterval) +} + +eventHandle = () => { + if (eventStore.length) { + eventStore.forEach((item) => { + item.tarList.forEach((el) => { + const { target, width, heighe } = el + const clientWidth = target.clientWidth + const clientHeight = target.clientHeight + const rWidth = clientWidth && width !== clientWidth + const rHeight = clientHeight && heighe !== clientHeight + + if (rWidth || rHeight) { + el.width = clientWidth + el.heighe = clientHeight + requestAnimationFrame(item.callback) + } + }) + }) + + eventListener() + } +} + +class ResizeObserverPolyfill { + constructor(callback, resizeInterval) { + this.tarList = [] + this.callback = callback + defaultInterval = resizeInterval + } + + observe(targetObj) { + if (targetObj) { + if (!this.tarList.includes(targetObj)) { + this.tarList.push({ + target: targetObj, + width: targetObj.clientWidth, + heighe: targetObj.clientHeight + }) + } + + if (!eventStore.length) { + eventListener() + } + + if (!eventStore.includes(this)) { + eventStore.push(this) + } + } + } + + unobserve(target) { + remove(eventStore, (item) => ~item.tarList.indexOf(target)) + } + + disconnect() { + remove(eventStore, (item) => item === this) + } +} + +const Resize = browserInfo.isDoc ? window.ResizeObserver || ResizeObserverPolyfill : ResizeObserverPolyfill + +export default Resize diff --git a/packages/vue/src/grid/src/utils/static/array/arrayEach.ts b/packages/vue/src/grid/src/utils/static/array/arrayEach.ts new file mode 100644 index 0000000000..6b03f5edf4 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/arrayEach.ts @@ -0,0 +1,38 @@ +/** + * 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. + * + */ + +const arrayEach = (obj, iterate, context) => { + if (obj) { + if (obj.forEach) { + obj.forEach(iterate, context) + } else { + for (let index = 0, len = obj.length; index < len; index++) { + iterate.call(context, obj[index], index, obj) + } + } + } +} + +export default arrayEach diff --git a/packages/vue/src/grid/src/utils/static/array/arrayIndexOf.ts b/packages/vue/src/grid/src/utils/static/array/arrayIndexOf.ts new file mode 100644 index 0000000000..faa3b6e66c --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/arrayIndexOf.ts @@ -0,0 +1,38 @@ +/** + * 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. + * + */ + +const arrayIndexOf = (obj, val) => { + if (obj.indexOf) { + return obj.indexOf(val) + } + + for (let index = 0, len = obj.length; index < len; index++) { + if (val === obj[index]) { + return index + } + } +} + +export default arrayIndexOf diff --git a/packages/vue/src/grid/src/utils/static/array/eachTree.ts b/packages/vue/src/grid/src/utils/static/array/eachTree.ts new file mode 100644 index 0000000000..e0da547af6 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/eachTree.ts @@ -0,0 +1,53 @@ +/** + * 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 helperCreateTreeFunc from './helperCreateTreeFunc' +import each from '../base/each' + +const eachTreeItem = ({ parent, obj, iterate, context, path, node, parseChildren, opts }) => { + each(obj, (item, index) => { + const paths = path.concat([`${index}`]) + const nodes = node.concat([item]) + iterate.call(context, item, index, obj, paths, parent, nodes) + + if (item && parseChildren) { + paths.push(parseChildren) + eachTreeItem({ + item, + obj: item[parseChildren], + context, + iterate, + node: nodes, + parseChildren, + path: paths, + opts + }) + } + }) +} + +const eachTree = helperCreateTreeFunc(eachTreeItem) + +export default eachTree diff --git a/packages/vue/src/grid/src/utils/static/array/every.ts b/packages/vue/src/grid/src/utils/static/array/every.ts new file mode 100644 index 0000000000..58e49b395a --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/every.ts @@ -0,0 +1,28 @@ +/** + * 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 helperCreateIterateHandleFn from './helperCreateIterateHandle' + +export default helperCreateIterateHandleFn('every', 1, 1, false, true) diff --git a/packages/vue/src/grid/src/utils/static/array/filterTree.ts b/packages/vue/src/grid/src/utils/static/array/filterTree.ts new file mode 100644 index 0000000000..ede41fc536 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/filterTree.ts @@ -0,0 +1,46 @@ +/** + * 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 eachTree from './eachTree' + +const filterTree = (obj, iterate, options, context) => { + let result = [] + + if (obj && iterate) { + eachTree( + obj, + (...args) => { + if (iterate.apply(context, args)) { + result.push(args[0]) + } + }, + options + ) + } + + return result +} + +export default filterTree diff --git a/packages/vue/src/grid/src/utils/static/array/find.ts b/packages/vue/src/grid/src/utils/static/array/find.ts new file mode 100644 index 0000000000..f065aa0aaf --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/find.ts @@ -0,0 +1,30 @@ +/** + * 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 helperCreateIterateHandle from './helperCreateIterateHandle' + +const find = helperCreateIterateHandle('find', 1, 3, true) + +export default find diff --git a/packages/vue/src/grid/src/utils/static/array/findTree.ts b/packages/vue/src/grid/src/utils/static/array/findTree.ts new file mode 100644 index 0000000000..597bc55bfb --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/findTree.ts @@ -0,0 +1,61 @@ +/** + * 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 helperCreateTreeFunc from './helperCreateTreeFunc' + +const findTreeItem = ({ parent, obj, iterate, context, path, node, parseChildren, opts }) => { + if (obj) { + for (let index = 0, len = obj.length; index < len; index++) { + const item = obj[index] + const paths = path.concat([`${index}`]) + const nodes = node.concat([item]) + + if (iterate.call(context, item, index, obj, paths, parent, nodes)) { + return { index, item, path, items: obj, parent, nodes } + } + + if (parseChildren && item) { + const newPath = paths.concat([parseChildren]) + const match = findTreeItem({ + parent: item, + obj: item[parseChildren], + iterate, + context, + path: newPath, + node: nodes, + parseChildren, + opts + }) + + if (match) { + return match + } + } + } + } +} + +const findTree = helperCreateTreeFunc(findTreeItem) + +export default findTree diff --git a/packages/vue/src/grid/src/utils/static/array/helperCreateIterateHandle.ts b/packages/vue/src/grid/src/utils/static/array/helperCreateIterateHandle.ts new file mode 100644 index 0000000000..c84340a115 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/helperCreateIterateHandle.ts @@ -0,0 +1,79 @@ +/** + * 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 hasOwnProp from '../base/hasOwnProp' + +const handle = ({ useArray, obj, iterate, context, matchValue, restIndex }) => { + let value + let flag = 0 + + if (useArray && Array.isArray(obj)) { + for (let index = 0, len = obj.length; index < len; index++) { + if (!!iterate.call(context, obj[index], index, obj) === matchValue) { + value = [true, false, index, obj[index]][restIndex] + flag = 1 + break + } + } + } else { + for (let key of Object.keys(obj)) { + if (hasOwnProp(obj, key)) { + if (!!iterate.call(context, obj[key], key, obj) === matchValue) { + value = [true, false, key, obj[key]][restIndex] + flag = 2 + break + } + } + } + } + + return { value, flag } +} + +const helperCreateIterateHandle = (prop, useArray, restIndex, matchValue, defaultValue) => (obj, iterate, context) => { + if (!obj || !iterate) { + return defaultValue + } + if (prop && obj[prop]) { + return obj[prop](iterate, context) + } else { + const ret = handle({ + useArray, + obj, + iterate, + context, + matchValue, + restIndex + }) + + if (ret.flag) { + return ret.value + } + } + + return defaultValue +} + +export default helperCreateIterateHandle diff --git a/packages/vue/src/grid/src/utils/static/array/helperCreateTreeFunc.ts b/packages/vue/src/grid/src/utils/static/array/helperCreateTreeFunc.ts new file mode 100644 index 0000000000..d5377b305f --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/helperCreateTreeFunc.ts @@ -0,0 +1,43 @@ +/** + * 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. + * + */ + +const helperCreateTreeFunc = (handle) => (obj, iterate, options, context?: unknown) => { + let opts = options || {} + let optChildren = opts.children || 'children' + const params = { + item: null, + obj, + iterate, + context, + path: [], + node: [], + parseChildren: optChildren, + opts + } + + return handle(params) +} + +export default helperCreateTreeFunc diff --git a/packages/vue/src/grid/src/utils/static/array/includes.ts b/packages/vue/src/grid/src/utils/static/array/includes.ts new file mode 100644 index 0000000000..4609ae3542 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/includes.ts @@ -0,0 +1,17 @@ +/** + * 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 indexOf from '../base/indexOf' + +const includes = (obj, val) => ~indexOf(obj, val) + +export default includes diff --git a/packages/vue/src/grid/src/utils/static/array/lastArrayEach.ts b/packages/vue/src/grid/src/utils/static/array/lastArrayEach.ts new file mode 100644 index 0000000000..fb688cc2e3 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/lastArrayEach.ts @@ -0,0 +1,32 @@ +/** + * 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. + * + */ + +const lastArrayEach = (obj, iterate, context) => { + for (let len = obj.length - 1; len >= 0; len--) { + iterate.call(context, obj[len], len, obj) + } +} + +export default lastArrayEach diff --git a/packages/vue/src/grid/src/utils/static/array/map.ts b/packages/vue/src/grid/src/utils/static/array/map.ts new file mode 100644 index 0000000000..48203ae007 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/map.ts @@ -0,0 +1,50 @@ +/** + * 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 each from '../base/each' +import isFunction from '../base/isFunction' +import property from '../function/property' + +const map = function (obj, iterate, context) { + let result = [] + + if (obj && arguments.length > 1) { + if (!isFunction(iterate)) { + iterate = property(iterate) + } + + if (obj.map) { + return obj.map(iterate, context) + } else { + each(obj, (...args) => { + result.push(iterate.apply(context, args)) + }) + } + } + + return result +} + +export default map diff --git a/packages/vue/src/grid/src/utils/static/array/mapTree.ts b/packages/vue/src/grid/src/utils/static/array/mapTree.ts new file mode 100644 index 0000000000..5ad867030b --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/mapTree.ts @@ -0,0 +1,56 @@ +/** + * 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 helperCreateTreeFunc from './helperCreateTreeFunc' +import map from './map' + +const mapTreeItem = ({ parent, obj, iterate, context, path, node, parseChildren, opts }) => { + let mapChildren = opts.mapChildren || parseChildren + + return map(obj, (item, index) => { + const paths = path.concat([`${index}`]) + const nodes = node.concat([item]) + const rest = iterate.call(context, item, index, obj, paths, parent, nodes) + + if (rest && item && parseChildren && item[parseChildren]) { + rest[mapChildren] = mapTreeItem({ + item, + obj: item[parseChildren], + iterate, + context, + path: paths, + node: nodes, + parseChildren, + opts + }) + } + + return rest + }) +} + +const mapTree = helperCreateTreeFunc(mapTreeItem) + +export default mapTree diff --git a/packages/vue/src/grid/src/utils/static/array/slice.ts b/packages/vue/src/grid/src/utils/static/array/slice.ts new file mode 100644 index 0000000000..de8b5e742d --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/slice.ts @@ -0,0 +1,38 @@ +/** + * 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. + * + */ + +const slice = (array, startIndex, endIndex) => { + let result = [] + + if (array) { + for (startIndex = startIndex || 0, endIndex = endIndex || array.length; startIndex < endIndex; startIndex++) { + result.push(array[startIndex]) + } + } + + return result +} + +export default slice diff --git a/packages/vue/src/grid/src/utils/static/array/sortBy.ts b/packages/vue/src/grid/src/utils/static/array/sortBy.ts new file mode 100644 index 0000000000..8d0532d0ea --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/sortBy.ts @@ -0,0 +1,107 @@ +/** + * 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 arrayEach from './arrayEach' +import toArray from './toArray' +import map from './map' +import isFunction from '../base/isFunction' +import isUndefined from '../base/isUndefined' +import isNull from '../base/isNull' +import get from '../base/get' +import property from '../function/property' + +// 支持中文、数字、字母排序 > null > undefined +const sortByDef = (v1, v2) => { + if (isUndefined(v1) || isUndefined(v2)) { + return isUndefined(v2) ? 1 : -1 + } + + if (isNull(v1) || isNull(v2)) { + return isNull(v2) ? 1 : -1 + } + + return v1 && v1.localeCompare ? v1.localeCompare(v2) : v1 > v2 ? 1 : -1 +} + +const sortMultis = (name, compares) => (item1, item2) => { + let v1 = item1[name] + let v2 = item2[name] + + if (v1 === v2) { + return compares ? compares(item1, item2) : 0 + } + + return sortByDef(v1, v2) +} + +const getSortPros = (arr, list, iterate, context) => { + iterate = Array.isArray(iterate) ? iterate : [iterate] + + arrayEach(iterate, (prop, index) => { + let eachCallback + + if (isFunction(prop)) { + eachCallback = (val, key) => { + val[index] = prop.call(context, val.data, key, arr) + } + } else { + eachCallback = (val) => { + val[index] = get(val.data, prop) + } + } + + arrayEach(list, eachCallback) + }) + + return iterate +} + +const sortBy = (arr, iterate, context, STR_UNDEFINED) => { + if (arr) { + if (iterate === STR_UNDEFINED) { + return toArray(arr).sort(sortByDef) + } + + let compares + let list = map(arr, (item) => ({ data: item })) + let sortPros = getSortPros(arr, list, iterate, context) + let len = sortPros.length + + if (len) { + while (len >= 0) { + compares = sortMultis(len, compares) + len-- + } + + list = list.sort(compares) + } + + return map(list, property('data')) + } + + return [] +} + +export default sortBy diff --git a/packages/vue/src/grid/src/utils/static/array/sum.ts b/packages/vue/src/grid/src/utils/static/array/sum.ts new file mode 100644 index 0000000000..1dc04765b9 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/sum.ts @@ -0,0 +1,55 @@ +/** + * 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 each from '../base/each' +import isFunction from '../base/isFunction' +import toNumber from '../number/toNumber' + +const sum = (array, iterate, context) => { + let result = 0 + let eachCallback + + if (iterate) { + if (isFunction(iterate)) { + eachCallback = (...args) => { + result += toNumber(iterate.apply(context, args)) + } + } else { + eachCallback = (val) => { + result += toNumber(val[iterate]) + } + } + } else { + eachCallback = (val) => { + result += toNumber(val) + } + } + + each(array, eachCallback) + + return result +} + +export default sum diff --git a/packages/vue/src/grid/src/utils/static/array/toArray.ts b/packages/vue/src/grid/src/utils/static/array/toArray.ts new file mode 100644 index 0000000000..5ecf90a098 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/toArray.ts @@ -0,0 +1,30 @@ +/** + * 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 map from './map' + +const toArray = (arr) => map(arr, (item) => item) + +export default toArray diff --git a/packages/vue/src/grid/src/utils/static/array/toTreeArray.ts b/packages/vue/src/grid/src/utils/static/array/toTreeArray.ts new file mode 100644 index 0000000000..335bff70b5 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/array/toTreeArray.ts @@ -0,0 +1,52 @@ +/** + * 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 each from '../base/each' +import assign from '../object/assign' + +const defaultOptions = { parentKey: 'parentId', key: 'id', children: 'children' } + +const unTreeList = (result, array, opts) => { + let optChildren = opts.children + let optData = opts.data + + each(array, (item) => { + const children = item[optChildren] + + if (optData) { + item = item[optData] + } + + result.push(item) + + children && unTreeList(result, children, opts) + }) + + return result +} + +const toTreeArray = (array, options) => unTreeList([], array, assign({}, defaultOptions, options)) + +export default toTreeArray diff --git a/packages/vue/src/grid/src/utils/static/base/clear.ts b/packages/vue/src/grid/src/utils/static/base/clear.ts new file mode 100644 index 0000000000..df9c7c1f53 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/clear.ts @@ -0,0 +1,78 @@ +/** + * 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 isNull from './isNull' +import isObject from './isObject' +import assign from '../object/assign' +import isPlainObject from './isPlainObject' +import objectEach from '../object/objectEach' +import helperDeleteProperty from './helperDeleteProperty' + +const clear = function (obj, defs, assigns) { + if (!obj) { + return obj + } + let isDefs = arguments.length > 1 && (isNull(defs) || !isObject(defs)) + let extds = isDefs ? assigns : defs + + if (isPlainObject(obj)) { + let eachCallback + + if (isDefs) { + eachCallback = (val, key) => { + obj[key] = defs + } + } else { + eachCallback = (val, key) => { + helperDeleteProperty(obj, key) + } + } + + objectEach(obj, eachCallback) + + if (extds) { + assign(obj, extds) + } + } else if (Array.isArray(obj)) { + if (isDefs) { + let len = obj.length + + while (len > 0) { + len-- + obj[len] = defs + } + } else { + obj.length = 0 + } + + if (extds) { + obj.push.apply(obj, extds) + } + } + + return obj +} + +export default clear diff --git a/packages/vue/src/grid/src/utils/static/base/clone.ts b/packages/vue/src/grid/src/utils/static/base/clone.ts new file mode 100644 index 0000000000..8d64675c7d --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/clone.ts @@ -0,0 +1,25 @@ +/** + * 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 isPlainObject from './isPlainObject' +import objectMap from '../object/objectMap' +import map from '../array/map' + +let deepClone +const startClone = (func, obj, deep) => func(obj, deep ? (val) => deepClone(val, deep) : (val) => val) + +deepClone = (val, deep) => + isPlainObject(val) ? startClone(objectMap, val, deep) : Array.isArray(val) ? startClone(map, val, deep) : val + +const clone = (obj, deep) => (obj ? deepClone(obj, deep) : obj) + +export default clone diff --git a/packages/vue/src/grid/src/utils/static/base/destructuring.ts b/packages/vue/src/grid/src/utils/static/base/destructuring.ts new file mode 100644 index 0000000000..554e7c4c7a --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/destructuring.ts @@ -0,0 +1,47 @@ +/** + * 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 keys from './keys' +import slice from '../array/slice' +import assign from '../object/assign' +import includes from '../array/includes' +import arrayEach from '../array/arrayEach' + +const destructuring = function (destination, sources) { + if (destination && sources) { + let rest = assign.apply(this, [{}].concat(slice(arguments, 1))) + let restKeys = keys(rest) + + arrayEach(keys(destination), (key) => { + if (includes(restKeys, key)) { + destination[key] = rest[key] + } + }) + } + + return destination +} + +export default destructuring diff --git a/packages/vue/src/grid/src/utils/static/base/each.ts b/packages/vue/src/grid/src/utils/static/base/each.ts new file mode 100644 index 0000000000..2db29d24cd --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/each.ts @@ -0,0 +1,37 @@ +/** + * 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 arrayEach from '../array/arrayEach' +import objectEach from '../object/objectEach' + +const each = (obj, iterate, context) => { + if (obj) { + return Array.isArray(obj) ? arrayEach(obj, iterate, context) : objectEach(obj, iterate, context) + } + + return obj +} + +export default each diff --git a/packages/vue/src/grid/src/utils/static/base/eqNull.ts b/packages/vue/src/grid/src/utils/static/base/eqNull.ts new file mode 100644 index 0000000000..237ad83d70 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/eqNull.ts @@ -0,0 +1,31 @@ +/** + * 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 isNullType from './isNull' +import isUndefinedType from './isUndefined' + +const eqNull = (object) => isNullType(object) || isUndefinedType(object) + +export default eqNull diff --git a/packages/vue/src/grid/src/utils/static/base/findIndexOf.ts b/packages/vue/src/grid/src/utils/static/base/findIndexOf.ts new file mode 100644 index 0000000000..c751370c46 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/findIndexOf.ts @@ -0,0 +1,38 @@ +/** + * 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 helperCreateiterateIndexOf from './helperCreateiterateIndexOf' + +const findIndexOf = helperCreateiterateIndexOf((obj, iterate, context) => { + for (let index = 0, len = obj.length; index < len; index++) { + if (iterate.call(context, obj[index], index, obj)) { + return index + } + } + + return -1 +}) + +export default findIndexOf diff --git a/packages/vue/src/grid/src/utils/static/base/get.ts b/packages/vue/src/grid/src/utils/static/base/get.ts new file mode 100644 index 0000000000..ebcbf950b3 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/get.ts @@ -0,0 +1,80 @@ +/** + * 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 eqNull from './eqNull' +import hasOwnProp from './hasOwnProp' +import isUndefined from './isUndefined' +import helperGetHGSKeys from './helperGetHGSKeys' +import staticHGKeyRE from '../static/staticHGKeyRE' + +const valGet = (obj, key) => { + const matchs = key ? key.match(staticHGKeyRE) : '' + + return matchs ? (matchs[1] ? (obj[matchs[1]] ? obj[matchs[1]][matchs[2]] : undefined) : obj[matchs[2]]) : obj[key] +} + +const pathGet = (obj, property) => { + if (!obj) { + return + } + + let rest + let index = 0 + + const getRest = (len, props) => { + for (rest = obj; index < len; index++) { + rest = valGet(rest, props[index]) + + if (eqNull(rest)) { + return + } + } + } + + if (obj[property] || hasOwnProp(obj, property)) { + return obj[property] + } else { + const props = helperGetHGSKeys(property) + const len = props.length + + if (len) { + getRest(len, props) + } + + return rest + } +} + +const get = (obj: object, property: string, defaultValue?: unknown) => { + if (eqNull(obj)) { + return defaultValue + } + + const result = pathGet(obj, property) + + return isUndefined(result) ? defaultValue : result +} + +export default get diff --git a/packages/vue/src/grid/src/utils/static/base/has.ts b/packages/vue/src/grid/src/utils/static/base/has.ts new file mode 100644 index 0000000000..bd11bc38c3 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/has.ts @@ -0,0 +1,94 @@ +/** + * 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 hasOwn from './hasOwnProp' +import staticHGKeyRE from '../static/staticHGKeyRE' +import helperGetHGSKeys from './helperGetHGSKeys' + +const formatterArgs = (matchs, rest, isHas) => { + let arrIndex = matchs[1] + let objProp = matchs[2] + + if (arrIndex) { + if (rest[arrIndex]) { + if (hasOwn(rest[arrIndex], objProp)) { + isHas = true + rest = rest[arrIndex][objProp] + } + } + } else { + if (hasOwn(rest, objProp)) { + isHas = true + rest = rest[objProp] + } + } + + return { isHas, rest } +} + +const has = (obj, property) => { + if (!obj) { + return + } + + const compare = (property) => { + let prop, matchs, rest, isHas + let props = helperGetHGSKeys(property) + let index = 0 + let len = props.length + let flag = false + + for (rest = obj; index < len; index++) { + isHas = false + prop = props[index] + matchs = prop ? prop.match(staticHGKeyRE) : '' + + if (matchs) { + const args = formatterArgs(matchs, rest, isHas) + + isHas = args.isHas + rest = args.rest + } else { + hasOwn(rest, prop) && ((isHas = true), (rest = rest[prop])) + } + + if (isHas) { + index === len - 1 && (flag = true) + } else { + break + } + } + + return flag + } + + if (hasOwn(obj, property)) { + return true + } else { + return compare(property) + } +} + +export default has diff --git a/packages/vue/src/grid/src/utils/static/base/hasOwnProp.ts b/packages/vue/src/grid/src/utils/static/base/hasOwnProp.ts new file mode 100644 index 0000000000..d364508105 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/hasOwnProp.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. + * + */ + +const hasOwnProp = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key) + +export default hasOwnProp diff --git a/packages/vue/src/grid/src/utils/static/base/helperCreateGetObjects.ts b/packages/vue/src/grid/src/utils/static/base/helperCreateGetObjects.ts new file mode 100644 index 0000000000..eda7066f5c --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/helperCreateGetObjects.ts @@ -0,0 +1,58 @@ +/** + * 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 each from './each' + +const helperCreateGetObjects = (name, getIndex) => { + let proMethod = Object[name] + + return (obj) => { + let result = [] + + if (!obj) { + return result + } + if (proMethod) { + return proMethod(obj) + } + + let eachCallback + + if (getIndex > 1) { + eachCallback = (key) => { + result.push([String(key), obj[key]]) + } + } else { + eachCallback = function () { + result.push(arguments[getIndex]) + } + } + each(obj, eachCallback) + + return result + } +} + +export default helperCreateGetObjects diff --git a/packages/vue/src/grid/src/utils/static/base/helperCreateInInObjectString.ts b/packages/vue/src/grid/src/utils/static/base/helperCreateInInObjectString.ts new file mode 100644 index 0000000000..35eb85708c --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/helperCreateInInObjectString.ts @@ -0,0 +1,34 @@ +/** + * 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. + * + */ + +let objectToString = Object.prototype.toString + +function helperCreateInInObjectString(type) { + return function (obj) { + return '[object ' + type + ']' === objectToString.call(obj) + } +} + +export default helperCreateInInObjectString diff --git a/packages/vue/src/grid/src/utils/static/base/helperCreateInTypeof.ts b/packages/vue/src/grid/src/utils/static/base/helperCreateInTypeof.ts new file mode 100644 index 0000000000..1433bd67ae --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/helperCreateInTypeof.ts @@ -0,0 +1,30 @@ +/** + * 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. + * + */ + +export default function helperCreateInTypeof(type) { + return function (obj) { + return typeof obj === type + } +} diff --git a/packages/vue/src/grid/src/utils/static/base/helperCreateIndexOf.ts b/packages/vue/src/grid/src/utils/static/base/helperCreateIndexOf.ts new file mode 100644 index 0000000000..79ef5dab26 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/helperCreateIndexOf.ts @@ -0,0 +1,46 @@ +/** + * 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. + * + */ + +const helperCreateIndexOf = (name, callback) => (obj, val) => { + if (!obj) { + return -1 + } + if (typeof obj === 'string' || Array.isArray(obj)) { + if (obj[name]) { + return obj[name](val) + } + + return callback(obj, val) + } + + for (let key of Object.keys(obj)) { + if (val === obj[key]) { + return key + } + } + return -1 +} + +export default helperCreateIndexOf diff --git a/packages/vue/src/grid/src/utils/static/base/helperCreateiterateIndexOf.ts b/packages/vue/src/grid/src/utils/static/base/helperCreateiterateIndexOf.ts new file mode 100644 index 0000000000..a44ad08a50 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/helperCreateiterateIndexOf.ts @@ -0,0 +1,47 @@ +/** + * 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 './isFunction' +import isString from './isString' +import hasOwnProp from './hasOwnProp' + +const helperCreateiterateIndexOf = (callback) => (obj, iterate, context) => { + if (!obj || !isFunction(iterate)) { + return -1 + } + if (Array.isArray(obj) || isString(obj)) { + return callback(obj, iterate, context) + } + + for (let key of Object.keys(obj)) { + if (hasOwnProp(obj, key) && iterate.call(context, obj[key], key, obj)) { + return key + } + } + + return -1 +} + +export default helperCreateiterateIndexOf diff --git a/packages/vue/src/grid/src/utils/static/base/helperDefaultCompare.ts b/packages/vue/src/grid/src/utils/static/base/helperDefaultCompare.ts new file mode 100644 index 0000000000..fe0ec93771 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/helperDefaultCompare.ts @@ -0,0 +1,30 @@ +/** + * 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. + * + */ + +function helperDefaultCompare(v1, v2) { + return v1 === v2 +} + +export default helperDefaultCompare diff --git a/packages/vue/src/grid/src/utils/static/base/helperDeleteProperty.ts b/packages/vue/src/grid/src/utils/static/base/helperDeleteProperty.ts new file mode 100644 index 0000000000..1e67e01c67 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/helperDeleteProperty.ts @@ -0,0 +1,34 @@ +/** + * 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. + * + */ + +const helperDeleteProperty = (obj, property) => { + try { + delete obj[property] + } catch (e) { + obj[property] = undefined + } +} + +export default helperDeleteProperty diff --git a/packages/vue/src/grid/src/utils/static/base/helperEqualCompare.ts b/packages/vue/src/grid/src/utils/static/base/helperEqualCompare.ts new file mode 100644 index 0000000000..218d2543ad --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/helperEqualCompare.ts @@ -0,0 +1,94 @@ +/** + * 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 { isDate, isRegExp } from '@opentiny/utils' +import isNumber from './isNumber' +import isString from './isString' +import isBoolean from './isBoolean' +import isUndefined from './isUndefined' +import keys from './keys' +import every from '../array/every' + +let equalVal + +const helperEqualCompare = ({ val1, val2, compare, func, key, obj1, obj2 }) => { + if (val1 === val2) { + return true + } + + if (val1 && val2 && !isNumber(val1) && !isNumber(val2) && !isString(val1) && !isString(val2)) { + if (isRegExp(val1)) { + return compare(String(val1), String(val2), key, obj1, obj2) + } + + if (isDate(val1) || isBoolean(val1)) { + return compare(Number(val1), Number(val2), key, obj1, obj2) + } + + return equalVal(val1, val2, func, key, compare) + } + + return compare(val1, val2, key, obj1, obj2) +} + +equalVal = (val1, val2, func, key, compare) => { + let result, val1Keys, val2Keys + let isObj1Arr = Array.isArray(val1) + let isObj2Arr = Array.isArray(val2) + + if (isObj1Arr || isObj2Arr ? isObj1Arr && isObj2Arr : val1.constructor === val2.constructor) { + val1Keys = keys(val1) + val2Keys = keys(val2) + + if (func) { + result = func(val1, val2, key) + } + + if (val1Keys.length === val2Keys.length) { + if (isUndefined(result)) { + return every( + val1Keys, + (key, index) => + key === val2Keys[index] && + helperEqualCompare({ + val1: val1[key], + val2: val2[val2Keys[index]], + compare, + func, + key: isObj1Arr || isObj2Arr ? index : key, + obj1: val1, + obj2: val2 + }) + ) + } + + return !!result + } + + return false + } +} + +export default helperEqualCompare diff --git a/packages/vue/src/grid/src/utils/static/base/helperGetHGSKeys.ts b/packages/vue/src/grid/src/utils/static/base/helperGetHGSKeys.ts new file mode 100644 index 0000000000..3e77c699ad --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/helperGetHGSKeys.ts @@ -0,0 +1,30 @@ +/** + * 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. + * + */ + +const helperGetHGSKeys = (property) => + // 以最快的方式判断数组,可忽略准确性 + property ? (property.splice && property.join ? property : String(property).split('.')) : [] + +export default helperGetHGSKeys diff --git a/packages/vue/src/grid/src/utils/static/base/indexOf.ts b/packages/vue/src/grid/src/utils/static/base/indexOf.ts new file mode 100644 index 0000000000..2c49a0667c --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/indexOf.ts @@ -0,0 +1,29 @@ +/** + * 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 helperCreateIndexOfFn from './helperCreateIndexOf' +import arrayIndexOfFn from '../array/arrayIndexOf' + +export default helperCreateIndexOfFn('indexOf', arrayIndexOfFn) diff --git a/packages/vue/src/grid/src/utils/static/base/isArray.ts b/packages/vue/src/grid/src/utils/static/base/isArray.ts new file mode 100644 index 0000000000..58f110fb15 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isArray.ts @@ -0,0 +1,36 @@ +/** + * 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 helperCreateInInObjectString from './helperCreateInInObjectString' + +/** + * 判断是否数组 + * + * @param {Object} obj 对象 + * @return {Boolean} + */ +let isArray = Array.isArray || helperCreateInInObjectString('Array') + +export default isArray diff --git a/packages/vue/src/grid/src/utils/static/base/isBoolean.ts b/packages/vue/src/grid/src/utils/static/base/isBoolean.ts new file mode 100644 index 0000000000..8fffc34268 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isBoolean.ts @@ -0,0 +1,28 @@ +/** + * 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. + * + */ + +const isBoolean = (obj) => typeof obj === 'boolean' + +export default isBoolean diff --git a/packages/vue/src/grid/src/utils/static/base/isDate.ts b/packages/vue/src/grid/src/utils/static/base/isDate.ts new file mode 100644 index 0000000000..50deb06d4b --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isDate.ts @@ -0,0 +1,35 @@ +/** + * 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 helperCreateInInObjectString from './helperCreateInInObjectString' + +/** + * 判断是否Date对象 + * + * @param {Object} obj 对象 + * @return {Boolean} + */ +let isDate = helperCreateInInObjectString('Date') + +export default isDate diff --git a/packages/vue/src/grid/src/utils/static/base/isEmpty.ts b/packages/vue/src/grid/src/utils/static/base/isEmpty.ts new file mode 100644 index 0000000000..b2e190deef --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isEmpty.ts @@ -0,0 +1,22 @@ +/** + * 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 isObject from './isObject' + +const isEmpty = (obj) => { + if (isObject(obj)) { + return Object.keys(obj).length === 0 + } + return true +} + +export default isEmpty diff --git a/packages/vue/src/grid/src/utils/static/base/isEqual.ts b/packages/vue/src/grid/src/utils/static/base/isEqual.ts new file mode 100644 index 0000000000..c12d9f37a5 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isEqual.ts @@ -0,0 +1,35 @@ +/** + * 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 helperEqlCompare from './helperEqualCompare' + +const isEqual = (obj1, obj2) => + helperEqlCompare({ + val1: obj1, + val2: obj2, + compare: (v1, v2) => v1 === v2 + }) + +export default isEqual diff --git a/packages/vue/src/grid/src/utils/static/base/isFunction.ts b/packages/vue/src/grid/src/utils/static/base/isFunction.ts new file mode 100644 index 0000000000..557c9ad924 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isFunction.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. + * + */ + +const isFunction = (obj) => typeof obj === 'function' + +export default isFunction diff --git a/packages/vue/src/grid/src/utils/static/base/isNaN.ts b/packages/vue/src/grid/src/utils/static/base/isNaN.ts new file mode 100644 index 0000000000..5296cf35d3 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isNaN.ts @@ -0,0 +1,30 @@ +/** + * 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 isNumber from './isNumber' + +const isNumberNaN = (obj) => isNumber(obj) && isNaN(obj) + +export default isNumberNaN diff --git a/packages/vue/src/grid/src/utils/static/base/isNull.ts b/packages/vue/src/grid/src/utils/static/base/isNull.ts new file mode 100644 index 0000000000..a6a4a5f514 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isNull.ts @@ -0,0 +1,28 @@ +/** + * 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. + * + */ + +const isNull = (object) => object === null + +export default isNull diff --git a/packages/vue/src/grid/src/utils/static/base/isNumber.ts b/packages/vue/src/grid/src/utils/static/base/isNumber.ts new file mode 100644 index 0000000000..308652e53d --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isNumber.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. + * + */ + +const isNumber = (obj) => typeof obj === 'number' + +export default isNumber diff --git a/packages/vue/src/grid/src/utils/static/base/isObject.ts b/packages/vue/src/grid/src/utils/static/base/isObject.ts new file mode 100644 index 0000000000..257c8822e2 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isObject.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. + * + */ + +const isObject = (obj) => typeof obj === 'object' && obj !== null + +export default isObject diff --git a/packages/vue/src/grid/src/utils/static/base/isPlainObject.ts b/packages/vue/src/grid/src/utils/static/base/isPlainObject.ts new file mode 100644 index 0000000000..9d9a51bdbc --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isPlainObject.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 { isPlainObject } from '@opentiny/utils' + +export default isPlainObject diff --git a/packages/vue/src/grid/src/utils/static/base/isRegExp.ts b/packages/vue/src/grid/src/utils/static/base/isRegExp.ts new file mode 100644 index 0000000000..a8d2a71432 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isRegExp.ts @@ -0,0 +1,36 @@ +/** + * 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 helperCreateInInObjectString from './helperCreateInInObjectString' + +/** + * 判断是否RegExp对象 + * + * @param {Object} obj 对象 + * @return {Boolean} + */ +let isRegExp = helperCreateInInObjectString('RegExp') + +export default isRegExp diff --git a/packages/vue/src/grid/src/utils/static/base/isSet.ts b/packages/vue/src/grid/src/utils/static/base/isSet.ts new file mode 100644 index 0000000000..7fa86a8092 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isSet.ts @@ -0,0 +1,29 @@ +/** + * 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. + * + */ +const supportSet = typeof Set !== 'undefined' + +const isSet = (obj) => supportSet && obj instanceof Set + +export default isSet diff --git a/packages/vue/src/grid/src/utils/static/base/isString.ts b/packages/vue/src/grid/src/utils/static/base/isString.ts new file mode 100644 index 0000000000..872d75732d --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isString.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. + * + */ + +const isString = (obj) => typeof obj === 'string' + +export default isString diff --git a/packages/vue/src/grid/src/utils/static/base/isUndefined.ts b/packages/vue/src/grid/src/utils/static/base/isUndefined.ts new file mode 100644 index 0000000000..57b11c7f25 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/isUndefined.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. + * + */ + +const isUndefined = (obj) => typeof obj === 'undefined' + +export default isUndefined diff --git a/packages/vue/src/grid/src/utils/static/base/keys.ts b/packages/vue/src/grid/src/utils/static/base/keys.ts new file mode 100644 index 0000000000..3cfb25342a --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/keys.ts @@ -0,0 +1,30 @@ +/** + * 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 helperCreateGetObjects from './helperCreateGetObjects' + +const keys = helperCreateGetObjects('keys', 1) + +export default keys diff --git a/packages/vue/src/grid/src/utils/static/base/lastEach.ts b/packages/vue/src/grid/src/utils/static/base/lastEach.ts new file mode 100644 index 0000000000..201274ac76 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/lastEach.ts @@ -0,0 +1,37 @@ +/** + * 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 lastArrayEach from '../array/lastArrayEach' +import lastObjectEach from '../object/lastObjectEach' + +const lastEach = (obj, iterate, context) => { + if (obj) { + return Array.isArray(obj) ? lastArrayEach(obj, iterate, context) : lastObjectEach(obj, iterate, context) + } + + return obj +} + +export default lastEach diff --git a/packages/vue/src/grid/src/utils/static/base/remove.ts b/packages/vue/src/grid/src/utils/static/base/remove.ts new file mode 100644 index 0000000000..93c986d0d5 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/remove.ts @@ -0,0 +1,74 @@ +/** + * 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 helperDeleteProperty from './helperDeleteProperty' +import isFunction from './isFunction' +import each from './each' +import arrayEach from '../array/arrayEach' +import lastEach from './lastEach' +import clear from './clear' +import eqNull from './eqNull' + +const pluckProperty = (name) => (obj, key) => key === name + +const remove = (obj, iterate, context) => { + if (obj) { + if (!eqNull(iterate)) { + let removeIndexs = [] + let rest = [] + + if (!isFunction(iterate)) { + iterate = pluckProperty(iterate) + } + + each(obj, (item, index, rest) => { + if (iterate.call(context, item, index, rest)) { + removeIndexs.push(index) + } + }) + + if (Array.isArray(obj)) { + lastEach(removeIndexs, (item) => { + rest.push(obj[item]) + obj.splice(item, 1) + }) + } else { + rest = {} + arrayEach(removeIndexs, (key) => { + rest[key] = obj[key] + helperDeleteProperty(obj, key) + }) + } + + return rest + } + + return clear(obj) + } + + return obj +} + +export default remove diff --git a/packages/vue/src/grid/src/utils/static/base/set.ts b/packages/vue/src/grid/src/utils/static/base/set.ts new file mode 100644 index 0000000000..c6c0f2ed72 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/set.ts @@ -0,0 +1,78 @@ +/** + * 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 helperGetHGSKeys from './helperGetHGSKeys' +import hasOwn from './hasOwnProp' + +const sKeyRE = /(.+)\[(\d+)\]$/ + +const valSet = (obj, key, isSet, value) => { + if (obj[key]) { + if (isSet) { + obj[key] = value + } + } else { + const matchs = key ? key.match(sKeyRE) : null + const rest = isSet ? value : {} + + if (matchs) { + const key = matchs[1] + const index = parseInt(matchs[2]) + + if (obj[key]) { + obj[key][index] = rest + } else { + obj[key] = new Array(index + 1) + obj[key][index] = rest + } + } else { + obj[key] = rest + } + + return rest + } + + return obj[key] +} + +const set = (obj, property, value) => { + if (obj) { + if (obj[property] || hasOwn(obj, property)) { + obj[property] = value + } else { + let rest = obj + let props = helperGetHGSKeys(property) + let len = props.length + + for (let index = 0; index < len; index++) { + rest = valSet(rest, props[index], index === len - 1, value) + } + } + } + + return obj +} + +export default set diff --git a/packages/vue/src/grid/src/utils/static/base/toJSONString.ts b/packages/vue/src/grid/src/utils/static/base/toJSONString.ts new file mode 100644 index 0000000000..4897696bcf --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/toJSONString.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. + * + */ + +const toJSONString = (obj) => JSON.stringify(obj) || '' + +export default toJSONString diff --git a/packages/vue/src/grid/src/utils/static/base/toStringJSON.ts b/packages/vue/src/grid/src/utils/static/base/toStringJSON.ts new file mode 100644 index 0000000000..788aa6e278 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/toStringJSON.ts @@ -0,0 +1,43 @@ +/** + * 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 from './isObject' +import isString from './isString' + +const toStringJSON = (str) => { + if (isObject(str)) { + return str + } else if (isString(str)) { + try { + return JSON.parse(str) + } catch (error) { + // do nothing + } + } + + return {} +} + +export default toStringJSON diff --git a/packages/vue/src/grid/src/utils/static/base/uniqueId.ts b/packages/vue/src/grid/src/utils/static/base/uniqueId.ts new file mode 100644 index 0000000000..5f5dc6426f --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/base/uniqueId.ts @@ -0,0 +1,17 @@ +/** + * 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. + * + */ + +let __uniqueId = 0 + +const uniqueId = (prefix) => (prefix ? String(prefix) : 0) + ++__uniqueId + +export default uniqueId diff --git a/packages/vue/src/grid/src/utils/static/browse/browse.ts b/packages/vue/src/grid/src/utils/static/browse/browse.ts new file mode 100644 index 0000000000..e4d5775577 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/browse/browse.ts @@ -0,0 +1,92 @@ +/** + * 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 staticStrUndefined from '../static/staticStrUndefined' +import staticDocument from '../static/staticDocument' +import staticWindow from '../static/staticWindow' +import assign from '../object/assign' +import arrayEach from '../array/arrayEach' + +function isBrowseStorage(storage) { + try { + let testKey = '__tiny_t' + + storage.setItem(testKey, 1) + storage.removeItem(testKey) + + return true + } catch (e) { + return false + } +} + +function isBrowseType(type) { + return navigator.userAgent.includes(type) +} + +/** + * 获取浏览器内核 + * @return Object + */ +function browse() { + let $body, isChrome, isEdge + let isMobile = false + let result = { + isNode: false, + isMobile, + isPC: false, + isDoc: !!staticDocument + } + + if (!staticWindow && typeof process !== staticStrUndefined) { + result.isNode = true + } else { + isEdge = isBrowseType('Edge') + isChrome = isBrowseType('Chrome') + isMobile = /(Android|webOS|iPhone|iPad|iPod|SymbianOS|BlackBerry|Windows Phone)/.test(navigator.userAgent) + + if (result.isDoc) { + $body = staticDocument.body || staticDocument.documentElement + arrayEach(['webkit', 'khtml', 'moz', 'ms', 'o'], (core) => { + result['-' + core] = !!$body[core + 'MatchesSelector'] + }) + } + + assign(result, { + edge: isEdge, + firefox: isBrowseType('Firefox'), + msie: !isEdge && result['-ms'], + safari: !isChrome && !isEdge && isBrowseType('Safari'), + isMobile, + isPC: !isMobile, + isLocalStorage: isBrowseStorage(staticWindow.localStorage), + isSessionStorage: isBrowseStorage(staticWindow.sessionStorage) + }) + } + + return result +} + +export default browse diff --git a/packages/vue/src/grid/src/utils/static/function/property.ts b/packages/vue/src/grid/src/utils/static/function/property.ts new file mode 100644 index 0000000000..b4a059f3a4 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/function/property.ts @@ -0,0 +1,29 @@ +/** + * 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 isNull from '../base/isNull' + +const property = (key, defs) => (object) => (isNull(object) ? defs : object[key]) + +export default property diff --git a/packages/vue/src/grid/src/utils/static/function/throttle.ts b/packages/vue/src/grid/src/utils/static/function/throttle.ts new file mode 100644 index 0000000000..0537a04eeb --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/function/throttle.ts @@ -0,0 +1,87 @@ +/** + * 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. + * + */ + +/** + * 创建一个策略函数,当被重复调用函数的时候,至少每隔多少秒毫秒调用一次该函数 + * + * @param {Function} callback 回调 + * @param {Number} wait 多少秒毫 + * @param {Object} options 参数{leading: 是否在之前执行, trailing: 是否在之后执行} + * @return {Function} + */ +function throttle(callback, wait, options) { + let args + let context + let ops = options || {} + let flag = false + let timeout = 0 + let optLeading = 'leading' in ops ? ops.leading : true + let optTrailing = 'trailing' in ops ? ops.trailing : false + let endFn + + const runFn = function () { + flag = true + callback.apply(context, args) + timeout = setTimeout(endFn, wait) + } + + endFn = function () { + timeout = 0 + + if (!flag && optTrailing === true) { + runFn() + } + } + + const cancelFn = function () { + let rest = timeout !== 0 + + clearTimeout(timeout) + flag = false + timeout = 0 + + return rest + } + + const throttled = function () { + args = arguments + context = this + flag = false + + if (timeout === 0) { + if (optLeading === true) { + runFn() + } else if (optTrailing === true) { + timeout = setTimeout(endFn, wait) + } + } + } + + throttled.cancel = cancelFn + + return throttled +} + +export default throttle diff --git a/packages/vue/src/grid/src/utils/static/index.ts b/packages/vue/src/grid/src/utils/static/index.ts new file mode 100644 index 0000000000..86dd95c6ce --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/index.ts @@ -0,0 +1,135 @@ +/** + * 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 assign from './object/assign' +import extend from './object/extend' +import objectEach from './object/objectEach' +import lastObjectEach from './object/lastObjectEach' +import objectMap from './object/objectMap' +import values from './object/values' + +// 数组相关的方法 +import map from './array/map' +import every from './array/every' +import find from './array/find' +import arrayEach from './array/arrayEach' +import lastArrayEach from './array/lastArrayEach' +import toArray from './array/toArray' +import sortBy from './array/sortBy' +import slice from './array/slice' +import includes from './array/includes' +import sum from './array/sum' +import toTreeArray from './array/toTreeArray' +import findTree from './array/findTree' +import eachTree from './array/eachTree' +import mapTree from './array/mapTree' +import filterTree from './array/filterTree' +import arrayIndexOf from './array/arrayIndexOf' + +// 基础方法 +import isNull from './base/isNull' +import isUndefined from './base/isUndefined' +import isFunction from './base/isFunction' +import isObject from './base/isObject' +import isString from './base/isString' +import isPlainObject from './base/isPlainObject' +import eqNull from './base/eqNull' +import each from './base/each' +import indexOf from './base/indexOf' +import keys from './base/keys' +import clone from './base/clone' +import lastEach from './base/lastEach' +import remove from './base/remove' +import clear from './base/clear' +import isBoolean from './base/isBoolean' +import isNumber from './base/isNumber' +import isEmpty from './base/isEmpty' +import isSet from './base/isSet' +import isEqual from './base/isEqual' +import uniqueId from './base/uniqueId' +import findIndexOf from './base/findIndexOf' +import toStringJSON from './base/toStringJSON' +import toJSONString from './base/toJSONString' +import has from './base/has' +import get from './base/get' +import set from './base/set' +import destructuring from './base/destructuring' + +// 数值相关方法 +import toNumber from './number/toNumber' + +// 字符串相关的方法 +import template from './string/template' +import toString from './string/toString' + +// 函数相关的方法 +import property from './function/property' + +const isArray = Array.isArray + +export { + assign, + extend, + objectEach, + lastObjectEach, + objectMap, + values, + map, + every, + find, + arrayEach, + lastArrayEach, + toArray, + sortBy, + slice, + includes, + sum, + toTreeArray, + findTree, + eachTree, + mapTree, + filterTree, + arrayIndexOf, + isArray, + isNull, + isUndefined, + isFunction, + isObject, + isString, + isPlainObject, + eqNull, + each, + indexOf, + keys, + clone, + lastEach, + remove, + clear, + isBoolean, + isNumber, + isEmpty, + isSet, + isEqual, + uniqueId, + findIndexOf, + toStringJSON, + toJSONString, + has, + get, + set, + destructuring, + toNumber, + template, + toString, + property +} diff --git a/packages/vue/src/grid/src/utils/static/number/helperCreateToNumber.ts b/packages/vue/src/grid/src/utils/static/number/helperCreateToNumber.ts new file mode 100644 index 0000000000..71a1985e17 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/number/helperCreateToNumber.ts @@ -0,0 +1,38 @@ +/** + * 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. + * + */ + +const helperCreateToNumber = (handle) => (str) => { + if (str) { + let num = handle(str) + + if (!isNaN(num)) { + return num + } + } + + return 0 +} + +export default helperCreateToNumber diff --git a/packages/vue/src/grid/src/utils/static/number/toNumber.ts b/packages/vue/src/grid/src/utils/static/number/toNumber.ts new file mode 100644 index 0000000000..434a3b2e86 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/number/toNumber.ts @@ -0,0 +1,30 @@ +/** + * 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 helperCreateToNumber from './helperCreateToNumber' + +const toNumber = helperCreateToNumber(parseFloat) + +export default toNumber diff --git a/packages/vue/src/grid/src/utils/static/object/assign.ts b/packages/vue/src/grid/src/utils/static/object/assign.ts new file mode 100644 index 0000000000..7be52eaaab --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/object/assign.ts @@ -0,0 +1,73 @@ +/** + * 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 arrayEach from '../array/arrayEach' +import keys from '../base/keys' +import clone from '../base/clone' + +let objectAssignFns = Object.assign + +const handleAssign = (destination, args, isClone) => { + let len = args.length + + for (let source, index = 1; index < len; index++) { + source = args[index] + + let eachCallback + + if (isClone) { + eachCallback = (key) => { + destination[key] = clone(source[key], isClone) + } + } else { + eachCallback = (key) => { + destination[key] = source[key] + } + } + + arrayEach(keys(args[index]), eachCallback) + } + + return destination +} + +const assign = function (target) { + if (target) { + let args = arguments + + if (target === true) { + if (args.length > 1) { + target = Array.isArray(target[1]) ? [] : {} + return handleAssign(target, args, true) + } + } else { + return objectAssignFns ? objectAssignFns.apply(Object, args) : handleAssign(target, args) + } + } + + return target +} + +export default assign diff --git a/packages/vue/src/grid/src/utils/static/object/extend.ts b/packages/vue/src/grid/src/utils/static/object/extend.ts new file mode 100644 index 0000000000..f2b0025186 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/object/extend.ts @@ -0,0 +1,17 @@ +/** + * 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 assign from './assign' + +const extend = assign + +export default extend diff --git a/packages/vue/src/grid/src/utils/static/object/lastObjectEach.ts b/packages/vue/src/grid/src/utils/static/object/lastObjectEach.ts new file mode 100644 index 0000000000..0e5a2acb1f --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/object/lastObjectEach.ts @@ -0,0 +1,35 @@ +/** + * 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 lastArrayEach from '../array/lastArrayEach' +import keys from '../base/keys' + +const lastObjectEach = (obj, iterate, context) => { + lastArrayEach(keys(obj), (key) => { + iterate.call(context, obj[key], key, obj) + }) +} + +export default lastObjectEach diff --git a/packages/vue/src/grid/src/utils/static/object/objectEach.ts b/packages/vue/src/grid/src/utils/static/object/objectEach.ts new file mode 100644 index 0000000000..959d6d8a3d --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/object/objectEach.ts @@ -0,0 +1,38 @@ +/** + * 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 hasOwnProp from '../base/hasOwnProp' + +const objectEach = (obj, iterate, context) => { + if (obj) { + Object.keys(obj).forEach((key) => { + if (hasOwnProp(obj, key)) { + iterate.call(context, obj[key], key, obj) + } + }) + } +} + +export default objectEach diff --git a/packages/vue/src/grid/src/utils/static/object/objectMap.ts b/packages/vue/src/grid/src/utils/static/object/objectMap.ts new file mode 100644 index 0000000000..e5c7a3c7f5 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/object/objectMap.ts @@ -0,0 +1,50 @@ +/** + * 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 each from '../base/each' +import isFunction from '../base/isFunction' +import property from '../function/property' + +const objectMap = (obj, iterate, context) => { + let result = {} + + if (obj) { + if (iterate) { + if (!isFunction(iterate)) { + iterate = property(iterate) + } + + each(obj, (val, index) => { + result[index] = iterate.call(context, val, index, obj) + }) + } else { + return obj + } + } + + return result +} + +export default objectMap diff --git a/packages/vue/src/grid/src/utils/static/object/values.ts b/packages/vue/src/grid/src/utils/static/object/values.ts new file mode 100644 index 0000000000..e7ff0b4bb2 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/object/values.ts @@ -0,0 +1,29 @@ +/** + * 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 objectEach from './objectEach' + +const values = (obj, iterator, ctx) => { + const objectValues = [] + + objectEach( + obj, + (val) => { + objectValues.push(val) + }, + ctx + ) + + return objectValues +} + +export default values diff --git a/packages/vue/src/grid/src/utils/static/static/staticDocument.ts b/packages/vue/src/grid/src/utils/static/static/staticDocument.ts new file mode 100644 index 0000000000..82d3661570 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/static/staticDocument.ts @@ -0,0 +1,30 @@ +/** + * 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 staticStrUndefined from './staticStrUndefined' + +let staticDocument = typeof document === staticStrUndefined ? 0 : document + +export default staticDocument diff --git a/packages/vue/src/grid/src/utils/static/static/staticHGKeyRE.ts b/packages/vue/src/grid/src/utils/static/static/staticHGKeyRE.ts new file mode 100644 index 0000000000..c892ab8c23 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/static/staticHGKeyRE.ts @@ -0,0 +1,28 @@ +/** + * 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. + * + */ + +const staticHGKeyRE = /(.+)?\[(\d+)\]$/ + +export default staticHGKeyRE diff --git a/packages/vue/src/grid/src/utils/static/static/staticParseInt.ts b/packages/vue/src/grid/src/utils/static/static/staticParseInt.ts new file mode 100644 index 0000000000..fcc3eb94b2 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/static/staticParseInt.ts @@ -0,0 +1,28 @@ +/** + * 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. + * + */ + +let staticParseInt = parseInt + +export default staticParseInt diff --git a/packages/vue/src/grid/src/utils/static/static/staticStrUndefined.ts b/packages/vue/src/grid/src/utils/static/static/staticStrUndefined.ts new file mode 100644 index 0000000000..db39d3e0df --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/static/staticStrUndefined.ts @@ -0,0 +1,28 @@ +/** + * 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. + * + */ + +let staticStrUndefined = 'undefined' + +export default staticStrUndefined diff --git a/packages/vue/src/grid/src/utils/static/static/staticWindow.ts b/packages/vue/src/grid/src/utils/static/static/staticWindow.ts new file mode 100644 index 0000000000..24d5f57c0d --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/static/staticWindow.ts @@ -0,0 +1,30 @@ +/** + * 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 staticStrUndefined from './staticStrUndefined' + +let staticWindow = typeof window === staticStrUndefined ? 0 : window + +export default staticWindow diff --git a/packages/vue/src/grid/src/utils/static/string/template.ts b/packages/vue/src/grid/src/utils/static/string/template.ts new file mode 100644 index 0000000000..755d52a3ba --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/string/template.ts @@ -0,0 +1,39 @@ +/** + * 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 toValString from './toString' +import get from '../base/get' + +const template = (str, obj) => { + let rest = toValString(str) + + if (rest && obj) { + return rest.replace(/\{{2}([.\w[\]\s]+)\}{2}/g, (match, keys) => get(obj, keys)) + } + + return rest +} + +export default template diff --git a/packages/vue/src/grid/src/utils/static/string/toString.ts b/packages/vue/src/grid/src/utils/static/string/toString.ts new file mode 100644 index 0000000000..01e51c90d7 --- /dev/null +++ b/packages/vue/src/grid/src/utils/static/string/toString.ts @@ -0,0 +1,40 @@ +/** + * 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 eqNull from '../base/eqNull' +import isNumber from '../base/isNumber' + +const toValString = (obj) => { + if (isNumber(obj)) { + if (String(obj).includes('e-')) { + let isNegative = obj < 0 + + return (isNegative ? '-' : '') + '0' + String((isNegative ? Math.abs(obj) : obj) + 1).substr(1) + } + } + + return String(eqNull(obj) ? '' : obj) +} + +export default toValString diff --git a/packages/vue/src/grid/src/utils/utils/column.ts b/packages/vue/src/grid/src/utils/utils/column.ts new file mode 100644 index 0000000000..3fd4b447a0 --- /dev/null +++ b/packages/vue/src/grid/src/utils/utils/column.ts @@ -0,0 +1,92 @@ +/** + * 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 { isBoolean } from '../static' +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 +} + +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 +} + +export const getColumnConfig = (context, options, config) => + context instanceof ColumnConfig ? context : new ColumnConfig(context, options, config) diff --git a/packages/vue/src/grid/src/utils/utils/common.ts b/packages/vue/src/grid/src/utils/utils/common.ts new file mode 100644 index 0000000000..81906952ed --- /dev/null +++ b/packages/vue/src/grid/src/utils/utils/common.ts @@ -0,0 +1,206 @@ +/** + * 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 { isNull } from '@opentiny/utils' +import { find } from '@opentiny/utils' +import { get, isFunction, set } from '../static' + +export const gridSize = ['medium', 'small', 'mini'] + +export const getSize = ({ size, $parent }) => size || ($parent && gridSize.includes($parent.size) ? $parent.size : null) + +export const getFuncText = (content) => (isFunction(content) ? content() : content) + +// 行主键 key +export const getRowkey = ($table) => $table.rowId + +// 行主键 value +export const getRowid = ($table, row) => { + const rowId = get(row, getRowkey($table)) + return rowId ? encodeURIComponent(rowId) : '' +} + +// 获取所有的列,排除分组 +export const getColumnList = (columns) => { + const result = [] + + columns.forEach((column) => { + if (column.children && column.children.length) { + result.push(...getColumnList(column.children)) + } else { + result.push(column) + } + }) + + return result +} + +export const getClass = (property, params) => (property ? (isFunction(property) ? property(params) : property) : '') + +export const getFilters = (filters) => + (filters || []).map(({ label, value, data, checked }) => ({ + label, + value, + data, + _data: data, + checked: !!checked + })) + +export const initFilter = (filter) => { + // 改成这种方式可以让用户配置一些筛选的默认行为,如果用户不配置就采用默认的 + return { + condition: { + input: '', + relation: 'equals', + empty: null, + type: null, + value: [] + }, + hasFilter: false, + custom: null, + ...filter + } +} + +export const formatText = (value) => `${isNull(value) ? '' : value}` + +export const setCellValue = (row, column, value) => { + const { format, property } = column + + // 处理异步列 + if (format && format.async && Array.isArray(format.data) && format.data.length > 0 && value) { + let labelText = '' + const { enabled, valueSplit, textSplit } = format.async.splitConfig || {} + const findCellValue = (optionValue) => + find(format.data, (col) => { + if (typeof col === 'object') { + const colLabel = get(col, format.async.text || 'label') + const colValue = get(col, format.async.value || 'value') + + col.label = colLabel + + return optionValue === colValue || optionValue === colLabel + } + + return optionValue === col + }) + + if (enabled) { + const labelTexts = [] + + value.split(valueSplit || ',').forEach((item) => { + const findValue = findCellValue(item) + + if (findValue) { + labelTexts.push(findValue.label) + } + }) + + labelText = labelTexts.join(textSplit || ',') + } else { + labelText = findCellValue(value) + } + + set(row, column.asyncPrefix + property, labelText ? labelText.label : labelText) + } + + set(row, property, value) +} + +export const hasChildrenList = (item) => item && item.children && item.children.length > 0 + +export const emitEvent = (vm, type, args) => { + if (vm.tableListeners[type]) { + const params = [].concat(args) + vm.$emit(type, ...params) + } +} + +/** + * 组装列配置,这里很重要,会触发表格collectColumn的watch,从而刷新表格 + * @param {Object} $table - 表格实例 + * @returns {void} + */ +export const assemColumn = ($table) => { + // 用于存储收集到的所有列配置 + const collectColumn = [] + + /** + * 递归组装列配置 + * @param {Array} columnVms - 列虚拟节点数组 + * @param {Array} columns - 用于存储组装后的列配置数组 + */ + const assem = (columnVms, columns) => { + // 检查columnVms是否为数组 + if (Array.isArray(columnVms)) { + // 遍历每个列虚拟节点 + columnVms.forEach((columnVm) => { + // 获取列的配置信息 + const column = columnVm.columnConfig + // 用于存储子列配置的数组 + const children = [] + + // 如果存在列配置 + if (column) { + // 将当前列配置添加到columns数组 + columns.push(column) + // 递归处理子列,将结果存储到children数组 + assem(columnVm.childColumns, children) + // 设置children属性: + // 1. 如果有子列,则设置为子列数组 + // 2. 如果没有子列,则设置为null(兼容旧版本实现) + column.children = children.length > 0 ? children : null + } + }) + } + } + + // 从表格实例的childColumns开始递归组装列配置 + assem($table.childColumns, collectColumn) + // 将组装好的列配置赋值给表格实例的collectColumn属性 + $table.collectColumn = collectColumn +} + +export const getCellValue = (row, column) => get(row, column.own.field) + +export const getListeners = ($attrs, $listeners) => { + const regHyphenate = /\B([A-Z])/g + const regEventPrefix = /^on[A-Z]/ + const listeners = {} + + if ($listeners) { + return $listeners + } + + Object.keys($attrs).forEach((name) => { + const event = $attrs[name] + + if (regEventPrefix.test(name) && typeof event === 'function') { + listeners[name.slice(2).replace(regHyphenate, '-$1').toLowerCase()] = event + } + }) + + return listeners +} diff --git a/packages/vue/src/grid/src/utils/utils/dom.ts b/packages/vue/src/grid/src/utils/utils/dom.ts new file mode 100644 index 0000000000..f319dc4007 --- /dev/null +++ b/packages/vue/src/grid/src/utils/utils/dom.ts @@ -0,0 +1,326 @@ +/** + * 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 { getRowid } from './common' +import { hasClass, getDomNode } from '@opentiny/utils' +import { getActualTarget } from '@opentiny/utils' +import { arrayIndexOf } from '../static' + +const ATTR_NAME = 'data-rowid' +const CELL_CLS = '.tiny-grid-cell' +const ROW_CLS = '.tiny-grid-body__row' + +export const isPx = (val) => val && /^\d+(px)?$/.test(val) + +export const isScale = (val) => val && /^\d+%$/.test(val) + +export const updateCellTitle = (event, td) => { + const cellEl = td ? td.querySelector(CELL_CLS) : event.currentTarget.querySelector(CELL_CLS) + const content = cellEl.innerText + + if (cellEl.getAttribute('title') !== content) { + cellEl.setAttribute('title', content) + } +} + +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 + } + } + } + }) +} + +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) + } + + return scrollLeft +} + +// 计算水平滚动位置(考虑存在冻结表的情况) +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 + } + }) + 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, move) => { + $table.$nextTick(() => { + const gridbodyEl = $table.$refs.tableBody.$el + const tdElem = gridbodyEl.querySelector(`.${column.id}`) + + if (tdElem) { + setBodyLeft(gridbodyEl, tdElem, $table, column, move) + } else if ($table.scrollXLoad) { + // 如果是虚拟渲染跨行滚动 + const visibleColumn = $table.visibleColumn + let scrollLeft = 0 + + for (let index = 0; index < visibleColumn.length; index++) { + if (visibleColumn[index] === column) { + break + } + + scrollLeft += visibleColumn[index].renderWidth + } + + gridbodyEl.scrollLeft = computeScrollLeft($table, { + _accumulateRenderWidth: scrollLeft, + _renderWidth: column.renderWidth + }) + } + }) +} + +export const hasDataTag = (el, value) => { + if (!el || !value) { + return false + } + + // 处理遇到 shadowRoot的情况 + if (el.host) { + el = el.host + } + + return (' ' + el.getAttribute('data-tag') + ' ').includes(' ' + value + ' ') +} + +export const getEventTargetNode = (event, container, queryCls) => { + let targetEl + let target = getActualTarget(event) + + while (target && target.nodeType && target !== document) { + if (queryCls && (hasClass(target, queryCls) || hasDataTag(target, queryCls))) { + targetEl = target + } else if (target === container) { + return { + flag: queryCls ? !!targetEl : true, + container, + targetElem: targetEl + } + } + + target = target.parentNode + } + + return { flag: false } +} + +function getNodeOffset(el, container, rest) { + if (el) { + const htmlEl = document.querySelector('html') + const bodyEl = document.body + const parentEl = el.parentNode + + rest.top += el.offsetTop + rest.left += el.offsetLeft + + if (parentEl && parentEl !== htmlEl && parentEl !== bodyEl) { + rest.top -= parentEl.scrollTop + rest.left -= parentEl.scrollLeft + } + + if (container && (el === container || el.offsetParent === container) ? 0 : el.offsetParent) { + return getNodeOffset(el.offsetParent, container, rest) + } + } + + return rest +} + +/** + * 获取元素相对于 document 的位置 + */ +export const getOffsetPos = (el, container) => getNodeOffset(el, container, { left: 0, top: 0 }) + +export const getAbsolutePos = (el) => { + const bounding = el.getBoundingClientRect() + const { scrollTop, scrollLeft } = getDomNode() + + return { + top: scrollTop + bounding.top, + left: scrollLeft + bounding.left + } +} + +/** + * 获取单元格节点索引 + */ +export const getCellNodeIndex = (cell) => { + const trEl = cell.parentNode + const columnIndex = arrayIndexOf(trEl.children, cell) + const rowIndex = arrayIndexOf(trEl.parentNode.children, trEl) + + return { columnIndex, rowIndex } +} + +/** + * 获取选中单元格矩阵范围 + */ +export const getRowNodes = (trList, cellNode, targetCellNode) => { + const startColIndex = cellNode.columnIndex + const startRowIndex = cellNode.rowIndex + const targetColIndex = targetCellNode.columnIndex + const targetRowIndex = targetCellNode.rowIndex + const rows = [] + + for ( + let rowIndex = Math.min(startRowIndex, targetRowIndex), rowLen = Math.max(startRowIndex, targetRowIndex); + rowIndex <= rowLen; + rowIndex++ + ) { + const cells = [] + const trEl = trList[rowIndex] + + for ( + let colIndex = Math.min(startColIndex, targetColIndex), colLen = Math.max(startColIndex, targetColIndex); + colIndex <= colLen; + colIndex++ + ) { + cells.push(trEl.children[colIndex]) + } + + rows.push(cells) + } + + return rows +} + +export const getCellIndexs = (cell) => { + const trEl = cell.parentNode + const rowid = trEl.getAttribute(ATTR_NAME) + const columnIndex = [].indexOf.call(trEl.children, cell) + const rowIndex = [].indexOf.call(trEl.parentNode.children, trEl) + + return { rowid, rowIndex, columnIndex } +} + +export const getCell = ($table, { row, column }) => + new Promise((resolve) => { + $table.$nextTick(() => { + const bodyElem = $table.$refs[`${column.fixed || 'table'}Body`] + + resolve( + (bodyElem || $table.$refs.tableBody).$el.querySelector( + `${ROW_CLS}[${ATTR_NAME}="${getRowid($table, row)}"] .${column.id}` + ) + ) + }) + }) + +export { getDomNode } diff --git a/packages/vue/src/grid/src/utils/utils/event.ts b/packages/vue/src/grid/src/utils/utils/event.ts new file mode 100644 index 0000000000..28478ab8fd --- /dev/null +++ b/packages/vue/src/grid/src/utils/utils/event.ts @@ -0,0 +1,67 @@ +/** + * 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 { browserInfo } from '@opentiny/utils' +import { remove } from '../static' +import { on } from '@opentiny/utils' + +// 监听全局事件 +const wheelName = browserInfo.isDoc && /Firefox/i.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' +const eventStore = [] + +const invoke = ({ comp, type, cb }, event) => { + if (type === event.type || (type === 'mousewheel' && event.type === wheelName)) { + cb.call(comp, event) + } +} + +const GlobalEvent = { + on(comp, type, cb, capture = false) { + if (cb) { + eventStore.push({ comp, type, cb, capture }) + } + }, + off(comp, type, capture = false) { + remove(eventStore, (item) => item.comp === comp && item.type === type && item.capture === capture) + }, + trigger(event) { + eventStore.filter((item) => !item.capture).forEach((item) => invoke(item, event)) + }, + capture(event) { + eventStore.filter((item) => item.capture).forEach((item) => invoke(item, event)) + } +} + +if (browserInfo.isDoc) { + on(document, 'keydown', GlobalEvent.trigger) + on(document, 'contextmenu', GlobalEvent.trigger) + on(window, 'mousedown', GlobalEvent.trigger) + on(window, 'mousedown', GlobalEvent.capture, true) + on(window, 'blur', GlobalEvent.trigger) + on(window, 'resize', GlobalEvent.trigger) + on(window, wheelName, GlobalEvent.trigger) +} + +export default GlobalEvent diff --git a/packages/vue/src/grid/src/utils/utils/index.ts b/packages/vue/src/grid/src/utils/utils/index.ts new file mode 100644 index 0000000000..4cd657047e --- /dev/null +++ b/packages/vue/src/grid/src/utils/utils/index.ts @@ -0,0 +1,18 @@ +/** + * 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 GlobalEvent from './event' + +export * from './column' +export * from './common' +export * from './dom' +export { GlobalEvent } diff --git a/packages/vue/src/grid/src/validator/src/methods.ts b/packages/vue/src/grid/src/validator/src/methods.ts index eddd96f650..4b27b6f6e1 100644 --- a/packages/vue/src/grid/src/validator/src/methods.ts +++ b/packages/vue/src/grid/src/validator/src/methods.ts @@ -100,11 +100,11 @@ const onRejected = (opt, _this) => { export default { // 对表格数据进行校验 - _validate(rows, cb) { + validate(rows, cb) { return this.beginValidate(rows, cb) }, // 与validate一致行为,区别就是会校验所有并返回所有不通过的列 - _fullValidate(rows, cb) { + fullValidate(rows, cb) { return this.beginValidate(rows, cb, true) }, // 聚焦到校验通过的单元格并弹出校验错误提示 @@ -262,7 +262,7 @@ export default { return new Promise(executor).then(onFulfilled).catch(onRejected) }, - _clearValidate() { + clearValidate() { Object.assign(this.validStore, { column: null, content: '', isArrow: false, row: null, rule: null, visible: false }) this.clostValidTooltip(undefined) @@ -333,8 +333,6 @@ export default { validTip.setExpectedState(true) this.activateTooltipValid(validTip) - } else if (isMessageInline) { - this.$nextTick(() => this.recalculate()) } emitEvent(this, 'valid-error', [params]) diff --git a/packages/vue/src/grid/src/validator/src/utils/beginValidate.ts b/packages/vue/src/grid/src/validator/src/utils/beginValidate.ts index 0639c7bb82..309d5b7dec 100644 --- a/packages/vue/src/grid/src/validator/src/utils/beginValidate.ts +++ b/packages/vue/src/grid/src/validator/src/utils/beginValidate.ts @@ -1,28 +1,4 @@ -/** - * 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 { isArray, isFunction, has, eachTree } from '@opentiny/vue-renderless/grid/static/' +import { isArray, isFunction, has, eachTree } from '../../../utils/static/' export function adjustParams(rows, cb, vaildDatas) { if (rows) { diff --git a/packages/vue/src/grid/src/virtual-scroll/index.ts b/packages/vue/src/grid/src/virtual-scroll/index.ts new file mode 100644 index 0000000000..ad8f9d35b9 --- /dev/null +++ b/packages/vue/src/grid/src/virtual-scroll/index.ts @@ -0,0 +1,19 @@ +/** + * 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 Methods from './src/methods' + +export default { + host: 'table', + install(host) { + Object.assign(host.methods, Methods) + } +} diff --git a/packages/vue/src/grid/src/virtual-scroll/src/methods.ts b/packages/vue/src/grid/src/virtual-scroll/src/methods.ts new file mode 100644 index 0000000000..8cc4a025b6 --- /dev/null +++ b/packages/vue/src/grid/src/virtual-scroll/src/methods.ts @@ -0,0 +1,398 @@ +import { browserInfo, debounce } from '@opentiny/utils' +import { getTotalRows } from '../../table/src/strategy' +import { toNumber } from '../../utils/static/' + +const isWebkit = browserInfo && browserInfo['-webkit'] +// 滚动加载防抖延迟时间(毫秒) +const DEBOUNCE_SCROLL_LOAD_DURATION = 200 +// 异步列数据收集超时时间(毫秒) +const ASYNC_COLLECT_TIMEOUT = 100 + +function computeScrollYLoad({ _vm, scrollLoad, scrollY, scrollYLoad, scrollYStore, tableBodyElem }) { + if (scrollYLoad || scrollLoad) { + // 获取表格体默认第一行的高度 + scrollYStore.rowHeight = _vm.rowHeight + } + + if (scrollYLoad) { + // scrollY.vSize用户配置的可视区域渲染行数 + const bodyHeight = toNumber( + tableBodyElem.style?.height || tableBodyElem.style?.maxHeight || tableBodyElem.clientHeight + ) + let visibleYSize = toNumber(scrollY.vSize || Math.ceil(bodyHeight / scrollYStore.rowHeight)) + + scrollYStore.visibleSize = visibleYSize + + // 自动优化 + if (!scrollY.oSize) { + scrollYStore.offsetSize = visibleYSize + } + + // scrollY.rSize用户配置的每次渲染行数 + if (!scrollY.rSize) { + // 如果是webkit内核浏览器则渲染行数+2,性能差的浏览器就*2,防止滚动时出现较多白屏 + scrollYStore.renderSize = visibleYSize + (isWebkit ? 2 : visibleYSize) + } + + // 计算需要渲染的表格数据,并更新YSpace元素高度用来显示正确的滚动条长度 + _vm.updateScrollYData() + } else { + _vm.updateScrollYSpace() + } +} + +function computeScrollXLoad({ _vm, scrollX, scrollXLoad, scrollXStore, tableBodyElem, visibleColumn }) { + if (scrollXLoad) { + /** + * 使用 “列渲染宽度累加方式” 优化默认 visibleSize 的计算, + * 旧的使用总宽度除以第一列宽度方式,会出现 visibleSize 很大出现渲染空白问题。 + */ + const clientWidth = tableBodyElem.clientWidth + let width = 0 + let visibleXSize = 0 + const len = visibleColumn.length + const colsWidth = visibleColumn?.map((i) => i.renderWidth).sort((a, b) => a - b) || [] + for (let i = 0; i < len; i++) { + width += colsWidth[i] + // 当虚拟滚动可见列宽度大于表格宽度或者循环结束,保存可见列大小 + if (width > clientWidth || i === len - 1) { + visibleXSize = i + 1 + break + } + } + + visibleXSize = toNumber(scrollX.vSize || visibleXSize) + + scrollXStore.visibleSize = visibleXSize + // 自动优化 + if (!scrollX.oSize) { + scrollXStore.offsetSize = visibleXSize + } + + if (!scrollX.rSize) { + scrollXStore.renderSize = visibleXSize + 2 + } + + // 处理x轴虚拟滚动渲染数据 + _vm.updateScrollXData() + } else { + _vm.updateScrollXSpace() + } +} + +export default { + // 计算可视渲染相关数据 + computeScrollLoad() { + return this.$nextTick().then(() => { + let { $refs, optimizeOpts, visibleColumn } = this as any + let { scrollLoad, scrollXLoad, scrollXStore, scrollYLoad, scrollYStore } = this as any + let { scrollX, scrollY } = optimizeOpts + let { tableBody } = $refs + let bodyElem = tableBody ? tableBody.$el : null + if (bodyElem) { + // 只计算X轴虚拟滚动逻辑,优化正常表格计算效率 + computeScrollXLoad({ _vm: this, scrollX, scrollXLoad, scrollXStore, tableBodyElem: bodyElem, visibleColumn }) + // 只计算Y轴虚拟滚动逻辑,优化正常表格计算效率 + computeScrollYLoad({ _vm: this, scrollLoad, scrollY, scrollYLoad, scrollYStore, tableBodyElem: bodyElem }) + } + this.$nextTick(this.updateStyle) + }) + }, + // 处理x轴方向虚拟滚动列数据加载 + updateScrollXData() { + let { scrollXStore } = this + + // 获取需要渲染的列数和最后一次渲染列的index值 + let ret = sliceVisibleColumn(this) + + if (ret.sliced) { + // 更新DOM样式保证表格滚动时的对齐,初始化表格时也需要计算x轴方向滚动条占位符的尺寸 + this.updateScrollXSpace() + // 处理滚动条滚动后的异步渲染列逻辑 + this.updateScrollStatus() + } + + this.debounceRaf('updateScrollXDataHandler', () => { + scrollXStore.lastStartIndex = ret.lastStartIndex + // 设置新的渲染列触发Vue渲染 + this.tableColumn = ret.tableColumn + this.visibleColumnChanged = ret.visibleColumnChanged + this.$nextTick(() => { + this.updateFooter() + this.updateStyle() + }) + }) + }, + // 更新横向 X 可视渲染上下剩余空间大小(续) + // 设置主表头/主表体/主表尾表格元素的marginLeft(已滚动出渲染范围的列,不渲染但是保留宽度占位,保证对齐) + updateScrollXSpace() { + const { elemStore, scrollXLoad, scrollXStore, scrollbarWidth, tableWidth, visibleColumn } = this + const { startIndex } = scrollXStore + let { bodyElem, footerElem, headerElem, leftSpaceWidth, marginLeft } = {} + + // 从缓存中获取主表头/主表体/主表尾表格元素 + headerElem = elemStore['main-header-table'] + bodyElem = elemStore['main-body-table'] + footerElem = elemStore['main-footer-table'] + + // 累加已滚动出渲染范围的列的总渲染宽度 + leftSpaceWidth = visibleColumn.slice(0, startIndex).reduce((previous, column) => { + // 左侧冻结列,不计算margin + if (column.fixed === 'left') return previous + return previous + column.renderWidth + }, 0) + marginLeft = scrollXLoad ? `${leftSpaceWidth}px` : '' + + // 设置主表头/主表体/主表尾表格元素的marginLeft(已滚动出渲染范围的列,不渲染但是保留宽度占位,保证对齐) + headerElem && (headerElem.style.marginLeft = marginLeft) + bodyElem.style.marginLeft = marginLeft + footerElem && (footerElem.style.marginLeft = marginLeft) + + // 设置各个区域的横向占位元素宽度 + const layouts = ['header', 'body', 'footer'] + layouts.forEach((layout) => { + const xSpaceElem = elemStore[`main-${layout}-xSpace`] + const extra = layout === 'header' ? scrollbarWidth : 0 + // 这里只能找到body中的元素,header和footer永远是false + if (xSpaceElem) { + // 表格主体内容x轴方向虚拟滚动条占位元素 + xSpaceElem.style.width = scrollXLoad ? `${tableWidth + extra}px` : '' + } + }) + + this.$nextTick(this.updateStyle) + }, + // 使用requestAnimationFrame实现防抖 + debounceRaf(handlerKey, callback) { + // 如果已有请求帧,先取消 + if (this[handlerKey]) { + cancelAnimationFrame(this[handlerKey]) + } + + // 请求新的动画帧 + this[handlerKey] = requestAnimationFrame(() => { + this[handlerKey] = null + callback() + }) + }, + // 处理虚拟滚动加载数据,并更新YSpace位置 + updateScrollYData() { + // 更新DOM样式保证表格滚动时的对齐 + this.updateScrollYSpace() + // 使用requestAnimationFrame优化渲染 + this.debounceRaf('updateScrollYDataHandler', () => { + this.handleTableData().then(() => this.$nextTick(this.updateStyle)) + }) + }, + // 更新纵向虚拟滚动 Y 可视渲染上下剩余空间大小 + updateScrollYSpace() { + let { $grid, elemStore, scrollLoad, scrollLoadStore, scrollYLoad } = this + let { rowHeight, startIndex } = this.scrollYStore + let totalRows = getTotalRows(this) + let bodyHeight = totalRows * rowHeight + let scrollHeight = $grid.pagerConfig ? $grid.pagerConfig.total * rowHeight : 0 + let isVScrollOrLoad = scrollYLoad || scrollLoad + let { marginTop, ySpaceHeight } = {} + + // 计算marginTop和空间高度 + marginTop = isVScrollOrLoad && scrollYLoad ? `${Math.max(startIndex * rowHeight, 0)}px` : '' + ySpaceHeight = isVScrollOrLoad ? `${bodyHeight}px` : '' + + // 存储滚动分页相关数据 + scrollLoadStore.bodyHeight = bodyHeight + scrollLoadStore.scrollHeight = scrollHeight + + const tableElem = elemStore['main-body-table'] + + // 使用transform设置表格偏移位置,更高效 + if (tableElem) { + tableElem.style.transform = marginTop ? `translateY(${marginTop})` : '' + } + + // 设置Y轴空间元素高度 + const ySpaceElem = elemStore['main-body-ySpace'] + ySpaceElem && (ySpaceElem.style.height = ySpaceHeight) + + // 滚动分页加载逻辑设置 + if (ySpaceElem && scrollLoad && $grid) { + Object.assign(scrollLoadStore, { bodyHeight, scrollHeight }) + ySpaceElem.firstChild.style.height = `${scrollHeight}px` + ySpaceElem.onscroll = this.debounceScrollLoad + } + }, + // 更新滚动加载条位置 + updateScrollLoadBar(event) { + let { $el, elemStore, scrollLoad, scrollLoadStore } = this + if (scrollLoad && $el.contains(event.target)) { + // 处理鼠标滚轮事件,更新滚动位置 + let wheelDelta = event.wheelDelta ? event.wheelDelta : -event.detail * 40 + let scrollElm = elemStore['main-body-ySpace'] + let { scrollHeight, bodyHeight } = scrollLoadStore + let max = scrollHeight - bodyHeight + let top = scrollElm.scrollTop - wheelDelta + + // 确保滚动位置在有效范围内 + top = max < top ? max : top + top = top < 0 ? 0 : top + scrollElm.scrollTop = top + } + }, + // 纵向 Y 可视渲染处理 + loadScrollYData(event) { + const { scrollYStore } = this as any + const { startIndex, renderSize, offsetSize, visibleIndex, visibleSize, rowHeight } = scrollYStore + + // 动态获取容器的scrollTop,这里有可能会造成卡顿,暂时没有好的方案 + let { scrollTop } = event.target + let toVisibleIndex = Math.ceil(scrollTop / rowHeight) + let preload = false + if (visibleIndex === toVisibleIndex) { + return + } + let marginSize = Math.min(Math.floor((renderSize - visibleSize) / 2), visibleSize) + if (toVisibleIndex < visibleIndex) { + // 向上 + preload = startIndex >= toVisibleIndex - offsetSize + if (preload) { + scrollYStore.startIndex = Math.max(0, toVisibleIndex - Math.max(marginSize, renderSize - visibleSize)) + this.updateScrollYData() + } + } else { + // 向下 + preload = startIndex + renderSize <= toVisibleIndex + visibleSize + offsetSize + if (preload) { + let totalRows = getTotalRows(this) + scrollYStore.startIndex = Math.max(0, Math.min(totalRows - renderSize, toVisibleIndex - marginSize)) + this.updateScrollYData() + } + } + scrollYStore.visibleIndex = toVisibleIndex + this.$nextTick(() => { + this.updateSelectedCls(true) + }) + }, + // 获取虚拟滚动状态 + getVirtualScroller() { + let { scrollXLoad, scrollYLoad } = this + let { scrollLeft, scrollTop } = this.$refs.tableBody.$el + return { + scrollX: scrollXLoad, + scrollY: scrollYLoad, + scrollLeft, + scrollTop + } + }, + // 横向 X 可视渲染事件处理 + triggerScrollXEvent(event) { + this.loadScrollXData(event) + }, + debounceScrollX(event) { + if (!this.tasks.debounceScrollX) { + this.tasks.debounceScrollXHandler = null + this.tasks.debounceScrollX = () => { + return requestAnimationFrame(() => { + this.tasks.debounceScrollXHandler = null + this.loadScrollXData(event) + }) + } + } + + if (this.tasks.debounceScrollXHandler) { + cancelAnimationFrame(this.tasks.debounceScrollXHandler) + this.tasks.debounceScrollXHandler = null + } + + this.tasks.debounceScrollXHandler = this.tasks.debounceScrollX() + }, + // 处理x轴滚动时,虚拟滚动数据计算 + loadScrollXData() { + let { scrollXStore, visibleColumn } = this + let { offsetSize, renderSize, startIndex, visibleIndex, visibleSize } = scrollXStore + let { scrollLeft } = this.$refs.tableBody.$el + let { preload = false, toVisibleIndex = 0, width = 0 } = {} + // 根据滚动位置计算边界可见列 + for (let i = 0; i < visibleColumn.length; i++) { + width += visibleColumn[i].renderWidth + if (scrollLeft < width) { + toVisibleIndex = i // 边界可见列索引 + break + } + } + // 边界可见列和上次记录的相同,滚动还没超过此列,就关闭Tooltip退出 + if (visibleIndex === toVisibleIndex) { + this.clostTooltip() + return + } + let marginSize = Math.min(Math.floor((renderSize - visibleSize) / 2), visibleSize) + marginSize = Math.max(0, marginSize) + if (visibleIndex > toVisibleIndex) { + // 向左 + preload = startIndex >= toVisibleIndex - offsetSize + if (preload) { + scrollXStore.startIndex = Math.max(0, toVisibleIndex - Math.max(marginSize, renderSize - visibleSize)) + this.updateScrollXData() + } + } else { + // 向右 + preload = startIndex + renderSize <= toVisibleIndex + visibleSize + offsetSize + if (preload) { + scrollXStore.startIndex = Math.max(0, Math.min(visibleColumn.length - renderSize, toVisibleIndex - marginSize)) + this.updateScrollXData() + } + } + scrollXStore.visibleIndex = toVisibleIndex + this.clostTooltip() + }, + // 纵向 Y 可视渲染事件处理 + triggerScrollYEvent(event) { + this.loadScrollYData(event) + }, + // 处理滚动分页相关逻辑 + debounceScrollLoad(event) { + if (!this.tasks.debounceScrollLoad) { + this.tasks.debounceScrollLoad = debounce(DEBOUNCE_SCROLL_LOAD_DURATION, () => { + const { scrollHeight, bodyHeight } = this.scrollLoadStore + const { currentPage, pageSize } = this.$grid.tablePage + const max = scrollHeight - bodyHeight + let scrollTop = event.target.scrollTop + + if (scrollTop > max) { + scrollTop = max + } + + const { rowHeight } = this.scrollYStore + let visibleIndex = Math.ceil(scrollTop / rowHeight) + let page = Math.ceil(visibleIndex / pageSize) + 1 + + if (currentPage !== page) { + this.$grid.pageCurrentChange(page) + } + }) + } + + this.tasks.debounceScrollLoad() + }, + /* X/Y 方向滚动状态更新 */ + updateScrollStatus() { + // 防抖处理滚动状态更新 + if (!this.tasks.updateScrollStatus) { + this.tasks.updateScrollStatus = debounce(ASYNC_COLLECT_TIMEOUT, () => { + const { scrollXLoad, scrollYLoad, isAsyncColumn } = this + + // 如果存在异步列并且开启了虚拟滚动 + if (isAsyncColumn && (scrollXLoad || scrollYLoad)) { + const { tableData, scrollXStore, scrollYStore, tableFullData, scrollDirection = 'N' } = this + const isInit = + (scrollXLoad && scrollXStore.visibleIndex === 0) || (scrollYLoad && scrollYStore.visibleIndex === 0) + + // 第一次初始化及横、纵向滚动时(用户直接设置 data 属性时将由 handleAsyncColumn 初始化异步列) + if (isInit || scrollDirection !== 'N') { + this.handleResolveColumn(tableFullData, this.collectAsyncColumn(tableData)) + } + } + }) + } + + this.tasks.updateScrollStatus() + } +} diff --git a/packages/vue/src/grid/src/virtual-scroll/src/types.ts b/packages/vue/src/grid/src/virtual-scroll/src/types.ts new file mode 100644 index 0000000000..d7e030d987 --- /dev/null +++ b/packages/vue/src/grid/src/virtual-scroll/src/types.ts @@ -0,0 +1,95 @@ +/** + * 滚动方向存储对象接口 + */ +export interface ScrollDirStore { + // 行高(Y方向) + rowHeight?: number + // 开始索引 + startIndex: number + // 渲染大小 + renderSize: number + // 偏移量大小 + offsetSize: number + // 可视索引 + visibleIndex: number + // 可视大小 + visibleSize: number +} + +/** + * 滚动加载存储对象接口 + */ +export interface ScrollLoadStore { + // 表格体高度 + bodyHeight: number + // 滚动高度 + scrollHeight: number +} + +/** + * 滚动配置选项接口 + */ +export interface ScrollConfig { + // 多少条数据时启用虚拟滚动 + gt?: number + // 当数据少于多少条时触发重新渲染 + oSize?: number + // 每次渲染的数据条数 + rSize?: number + // 可视区域的数据条数 + vSize?: number + // 行高(Y方向) + rHeight?: number + // 自适应最优渲染方式 + adaptive?: boolean +} + +/** + * 优化选项接口 + */ +export interface OptimizeOptions { + // 动画 + animat?: boolean + // 延迟hover处理 + delayHover?: number + // X方向滚动配置 + scrollX?: ScrollConfig + // Y方向滚动配置 + scrollY?: ScrollConfig +} + +/** + * 表格列接口 + */ +export interface TableColumn { + // 列ID + id: string + // 属性名 + property: string + // 渲染宽度 + renderWidth: number + // 其他属性... + [key: string]: any +} + +/** + * 表格行接口 + */ +export interface TableRow { + // 表格行的键值对数据 + [key: string]: any +} + +/** + * 表格元素存储接口 + */ +export interface ElementStore { + // 主体表格元素 + 'main-body-table'?: HTMLElement + // Y轴空间元素 + 'main-body-ySpace'?: HTMLElement + // X轴空间元素 + 'main-body-xSpace'?: HTMLElement + // 其他元素... + [key: string]: any +} diff --git a/packages/vue/src/grid/src/virtual-scroll/src/utils.ts b/packages/vue/src/grid/src/virtual-scroll/src/utils.ts new file mode 100644 index 0000000000..ac02c659b1 --- /dev/null +++ b/packages/vue/src/grid/src/virtual-scroll/src/utils.ts @@ -0,0 +1,69 @@ +import type { ScrollConfig } from './types' + +/** + * 合并滚动方向存储对象 + * @param {Object} options - 用户配置的滚动方向选项 + * @param {Object} scrollStore - 滚动方向存储对象 + */ +export function mergeScrollDirStore(options = {}, scrollStore: any) { + // 有配置才进行合并 + if (options) { + const { oSize, rSize, rHeight } = options as ScrollConfig + + if (oSize) { + scrollStore.offsetSize = oSize + } + + if (rSize) { + scrollStore.renderSize = rSize + } + + if (rHeight) { + scrollStore.rowHeight = rHeight + } + } +} + +/** + * 计算表格内部元素的位置 + * @param {Object} params - 参数对象 + */ +export function generateFixedClassName({ + $table, + bodyElem, + leftList, + rightList +}: { + $table: any + bodyElem: HTMLElement + leftList: any[] + rightList: any[] +}) { + const { scrollLeft } = bodyElem + + let isLeftActive = leftList.length && scrollLeft > 0 + let isRightActive = rightList.length && bodyElem.clientWidth < bodyElem.scrollWidth - scrollLeft + + if (isLeftActive) { + $table.scrollDirection = 'L' + } else if (isRightActive) { + $table.scrollDirection = 'R' + } else { + $table.scrollDirection = 'N' + } + + // 优化: 当方向改变时才修改类,否则跳过 + if (isLeftActive) { + // 存在左侧固定列且有水平滚动 + bodyElem.classList.add('tiny-grid__body--scrolling-left') + } else { + bodyElem.classList.remove('tiny-grid__body--scrolling-left') + } + + if (isRightActive) { + // 存在右侧固定列且有水平滚动 + bodyElem.classList.add('tiny-grid__body--scrolling-right') + } else { + bodyElem.classList.remove('tiny-grid__body--scrolling-right') + } +}