diff --git a/packages/renderless/src/grid/utils/common.ts b/packages/renderless/src/grid/utils/common.ts index 09bb02215..79b640f65 100644 --- a/packages/renderless/src/grid/utils/common.ts +++ b/packages/renderless/src/grid/utils/common.ts @@ -47,7 +47,11 @@ export const getColumnList = (columns) => { const result = [] columns.forEach((column) => { - result.push.apply(result, column.children && column.children.length ? getColumnList(column.children) : [column]) + if (column.children && column.children.length) { + result.push(...getColumnList(column.children)) + } else { + result.push(column) + } }) return result @@ -137,9 +141,10 @@ export const destroyColumn = ($table, { columnConfig }) => { $table.collectColumn = $table.collectColumn.slice(0) } -export const emitEvent = (vnode, type, args) => { - if (vnode.tableListeners[type]) { - vnode.$emit.apply(vnode, [type].concat(args)) +export const emitEvent = (vm, type, args) => { + if (vm.tableListeners[type]) { + const params = [].concat(args) + vm.$emit(type, ...params) } } @@ -182,7 +187,7 @@ export const getListeners = ($attrs, $listeners) => { const event = $attrs[name] if (regEventPrefix.test(name) && typeof event === 'function') { - listeners[name.substr(2).replace(regHyphenate, '-$1').toLowerCase()] = event + listeners[name.slice(2).replace(regHyphenate, '-$1').toLowerCase()] = event } }) diff --git a/packages/vue/src/grid/index.ts b/packages/vue/src/grid/index.ts index 8e60e43d2..e0cd09fdb 100644 --- a/packages/vue/src/grid/index.ts +++ b/packages/vue/src/grid/index.ts @@ -25,18 +25,78 @@ import Filter from './src/filter' import GridConfig from './src/config' import GridRadio from './src/radio' import GridButton from './src/button' +import FetchData from './src/fetch-data' +import Pager from './src/pager' +import Toolbar from './src/toolbar' +import ColumnAnchor from './src/column-anchor' +import Dragger from './src/dragger' +import Sort from './src/sort' +import Tooltip from './src/tooltip' +import Checkbox from './src/checkbox' +import Tree from './src/tree' import * as GridTools from './src/tools' import { version } from './package.json' +import type { Plugin } from './src/types/index.type' -// 右键菜单、内置编辑器、导出、键盘操作、校验、响应式改变表格宽高(auto-resize)、筛选 -const components = [Menu, Edit, Export, Keyboard, Validator, Resize, Filter] +/** + * Menu 右键菜单 + * Edit 内置编辑器 + * Export 导出 + * Keyboard 键盘操作 + * Validator 校验 + * Resize 响应式改变表格宽高(auto-resize) + * Filter 筛选 + * FetchData 远程数据处理 + * Pager 分页处理逻辑 + * Toolbar 工具栏处理逻辑 + * ColumnAnchor 表格列锚点 + * Dragger 拖拽相关逻辑 + * Sort 排序相关逻辑 + * Tooltip 提示相关逻辑 + * Checkbox 多选相关逻辑 + * Tree 树表相关逻辑 + */ +const plugins: Plugin[] = [ + Menu, + Edit, + Export, + Keyboard, + Validator, + Resize, + Filter, + FetchData, + Pager, + Toolbar, + ColumnAnchor, + Dragger, + Sort, + Tooltip, + Checkbox, + Tree +] // 设置全局参数,配置GlobalConfig,提供比如国际化方法 GridAdapter.setup({ i18n: t }) GridAdapter.t = t -// 把各个插件的方法都合并会$table -components.map((component) => component.install(Table)) +// 将每个插件的方法都合并回自己的宿主组件 +plugins.map((plugin) => plugin.install(plugin.host === 'grid' ? Grid : Table)) + +// 让用户可以通过grid组件的方法间接调用内层table组件的方法 +const getWrapFunc = (name) => + function (...args) { + const tinyTable = this.$refs.tinyTable + if (tinyTable) { + return this.$refs.tinyTable[name].apply(tinyTable, args) + } + } + +// 将table组件的方法,传递给grid组件使用,this指向全部指向tinyTable +Object.keys(Table.methods).forEach((name) => { + if (!Grid.methods[name]) { + Grid.methods[name] = getWrapFunc(name) + } +}) Grid.version = version diff --git a/packages/vue/src/grid/src/checkbox/index.ts b/packages/vue/src/grid/src/checkbox/index.ts new file mode 100644 index 000000000..ad8f9d35b --- /dev/null +++ b/packages/vue/src/grid/src/checkbox/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/table/src/utils/handleSelectRow.ts b/packages/vue/src/grid/src/checkbox/src/handleSelectRow.ts similarity index 100% rename from packages/vue/src/grid/src/table/src/utils/handleSelectRow.ts rename to packages/vue/src/grid/src/checkbox/src/handleSelectRow.ts diff --git a/packages/vue/src/grid/src/checkbox/src/methods.ts b/packages/vue/src/grid/src/checkbox/src/methods.ts new file mode 100644 index 000000000..a779e8808 --- /dev/null +++ b/packages/vue/src/grid/src/checkbox/src/methods.ts @@ -0,0 +1,267 @@ +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/' + +export default { + // 处理默认勾选 + handleSelectionDefChecked() { + let fullDataRowIdData = this.fullDataRowIdData + let { checkAll, checkRowKeys } = this.selectConfig || {} + + if (checkAll) { + this.setAllSelection(true) + return + } + if (checkRowKeys) { + let defCheckedRowids = checkRowKeys.map((key) => encodeURIComponent(key)) + let defCheckedRows = [] + + defCheckedRowids.forEach((rowid) => { + let rowCache = fullDataRowIdData[rowid] + if (rowCache) { + defCheckedRows.push(rowCache.row) + } + }) + + this.setSelection(defCheckedRows, true) + } + }, + setSelection(rows, value) { + if (rows) { + if (!isArray(rows)) { + rows = [rows] + } + rows.forEach((row) => this.handleSelectRow({ row }, !!value)) + } + return this.$nextTick() + }, + // 多选,行选中事件。value:选中true、不选false、不确定-1 + handleSelectRow({ row }, value) { + hasCheckField({ row }, value, this) + hasNoCheckField({ row }, value, this) + this.checkSelectionStatus() + }, + handleToggleCheckRowEvent(params, event) { + let selection = this.selection + let { checkField } = this.selectConfig || {} + let { row } = params + let value = checkField ? !get(row, checkField) : !~selection.indexOf(row) + if (event) { + this.triggerCheckRowEvent(event, params, value) + } else { + this.handleSelectRow(params, value) + } + }, + triggerCheckRowEvent(event, params, value) { + let { selectConfig = {} } = this + let { checkMethod } = selectConfig + if (!checkMethod || checkMethod(params)) { + this.handleSelectRow(params, value) + emitEvent(this, 'select-change', [ + { + selection: this.getSelectRecords(), + checked: value, + $table: this, + ...params + }, + event + ]) + } + }, + // 多选,切换某一行的选中状态 + toggleRowSelection(row) { + this.handleToggleCheckRowEvent({ row }) + return this.$nextTick() + }, + setAllSelection(value) { + let { afterFullData, selectConfig = {}, treeConfig, selection } = this + let { checkField: property, reserve, checkStrictly, checkMethod } = selectConfig + hasCheckFieldNoStrictly({ afterFullData, checkMethod, checkStrictly, property, selection, treeConfig, value }) + let selectRows = hasNoCheckFieldNoStrictly({ + afterFullData, + checkMethod, + checkStrictly, + property, + selection, + treeConfig, + value + }) + setSelectionNoStrictly({ _vm: this, checkStrictly, reserve, selectRows, selection, value }) + this.treeIndeterminates = [] + this.checkSelectionStatus() + }, + checkSelectionStatus() { + let { afterFullData, selection, treeIndeterminates } = this + let { checkField, checkStrictly, checkMethod } = this.selectConfig || {} + let { everyHandler, someHandler } = {} + if (checkStrictly) { + return + } + // 包含新增的数据 + if (checkField) { + everyHandler = checkMethod + ? (row, rowIndex) => !checkMethod({ row, rowIndex }) || get(row, checkField) + : (row) => get(row, checkField) + someHandler = (row) => get(row, checkField) || ~treeIndeterminates.indexOf(row) + this.isAllSelected = false + afterFullData.length && (this.isAllSelected = afterFullData.every(everyHandler)) + this.isIndeterminate = !this.isAllSelected && afterFullData.some(someHandler) + } else { + everyHandler = (row, rowIndex) => !checkMethod({ row, rowIndex }) + this.headerCheckDisabled = checkMethod && afterFullData.length && afterFullData.every(everyHandler) + everyHandler = checkMethod + ? (row, rowIndex) => !checkMethod({ row, rowIndex }) || ~selection.indexOf(row) + : (row) => ~selection.indexOf(row) + someHandler = (row) => ~treeIndeterminates.indexOf(row) || ~selection.indexOf(row) + this.isAllSelected = false + afterFullData.length && (this.isAllSelected = afterFullData.every(everyHandler)) + this.isIndeterminate = !this.isAllSelected && afterFullData.some(someHandler) + } + }, + // 保留选中状态 + reserveCheckSelection() { + let { fullDataRowIdData, selection } = this + let { reserve } = this.selectConfig || {} + let rowkey = getTableRowKey(this) + if (reserve && selection.length) { + this.selection = selection.map((row) => { + let rowCache = fullDataRowIdData[`${get(row, rowkey)}`] + return rowCache ? rowCache.row : row + }) + } + }, + // 多选,选中所有事件 + triggerCheckAllEvent(event, value) { + this.setAllSelection(value) + let eventParams = { + selection: this.getSelectRecords(), + checked: value, + $table: this + } + emitEvent(this, 'select-all', [eventParams, event]) + }, + // 多选,切换所有行的选中状态 + toggleAllSelection() { + this.triggerCheckAllEvent(null, !this.isAllSelected) + return this.$nextTick() + }, + clearSelection() { + let { tableFullData, treeConfig } = this + let { checkField } = this.selectConfig || {} + if (checkField) { + treeConfig + ? eachTree(tableFullData, (item) => set(item, checkField, false), treeConfig) + : tableFullData.forEach((item) => set(item, checkField, false)) + } + Object.assign(this, { + isAllSelected: false, + isIndeterminate: false, + selection: [], + treeIndeterminates: [] + }) + + return this.$nextTick() + }, + initMultipleHistory() { + const { isMultipleHistory, toolBarVm } = this.$grid + const { + settingOpts: { storageKey }, + id: toolbarId + } = toolBarVm + let remoteSelectedMethod = toolBarVm.setting.multipleHistory.remoteSelectedMethod + let remoteSelectedPromise + + if ( + isMultipleHistory && + toolBarVm && + toolBarVm.setting && + toolBarVm.setting.multipleHistory && + remoteSelectedMethod + ) { + if (typeof remoteSelectedMethod === 'function') { + remoteSelectedPromise = remoteSelectedMethod() + + if (typeof remoteSelectedPromise.then === 'function') { + remoteSelectedPromise.then((storeStr) => { + let storeObj = toStringJSON(storeStr) + storeObj = (storeObj && storeObj[storageKey]) || null + storeObj = (storeObj || {})[toolbarId] || {} + const { columns, pageSize } = storeObj + toolBarVm.applySettings({ columns, pageSize }) + }) + } + } + } + }, + // 显示多选工具栏 + showSelectToolbar() { + let { + $grid: { selectToolbar, showHeader }, + selectToolbarStore + } = this + if (selectToolbar && showHeader) { + selectToolbarStore.visible = false + let selectColumn = find(this.visibleColumn, (item) => item.type === 'selection') + 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') + 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 adjust = 1 + if (selectColumn.fixed === 'right') { + range.setStart(tr, thArr.indexOf(selectTh)) + range.setEnd(tr, thArr.length) + rangeBoundingRect = range.getBoundingClientRect() + layout.left = `${adjust}px` + } else { + range.setStart(tr, 0) + range.setEnd(tr, thArr.indexOf(selectTh) + 1) + rangeBoundingRect = range.getBoundingClientRect() + layout.left = `${rangeBoundingRect.width + adjust}px` + } + layout.width = `${headerBoundingRect.width - rangeBoundingRect.width - 2 * adjust}px` + if (!selectColumn.fixed && position === 'left') { + range = document.createRange() + range.setStart(tr, 0) + range.setEnd(tr, thArr.indexOf(selectTh)) + rangeBoundingRect = range.getBoundingClientRect() + layout.left = `${adjust}px` + layout.width = `${rangeBoundingRect.width - 2 * adjust}px` + } + layout.top = `${headerBoundingRect.height - rangeBoundingRect.height + adjust}px` + layout.height = `${rangeBoundingRect.height - 2 * adjust}px` + return this.$nextTick().then(() => { + selectToolbarStore.layout = layout + selectToolbarStore.visible = true + }) + } + } + return this.$nextTick() + }, + // 切换多选工具栏的显示 + toggleSelectToolbarVisible() { + this.selectToolbarStore.visible = !this.selectToolbarStore.visible + return this.$nextTick() + }, + // 在空数据时Selection列表头复选框禁用,headerAutoDisabled设置为false就会和旧版本兼容 + handleSelectionHeader() { + const { tableFullData, visibleColumn, selectConfig = {} } = this + const { headerAutoDisabled } = selectConfig + const selectionColumn = visibleColumn.find((column) => column.type === 'selection') + if ( + (typeof headerAutoDisabled === 'undefined' || (typeof headerAutoDisabled === 'boolean' && headerAutoDisabled)) && + !tableFullData.length && + selectionColumn + ) { + this.headerCheckDisabled = true + } + } +} diff --git a/packages/vue/src/grid/src/table/src/utils/setAllSelection.ts b/packages/vue/src/grid/src/checkbox/src/setAllSelection.ts similarity index 100% rename from packages/vue/src/grid/src/table/src/utils/setAllSelection.ts rename to packages/vue/src/grid/src/checkbox/src/setAllSelection.ts diff --git a/packages/vue/src/grid/src/column-anchor/index.ts b/packages/vue/src/grid/src/column-anchor/index.ts new file mode 100644 index 000000000..3a765dc52 --- /dev/null +++ b/packages/vue/src/grid/src/column-anchor/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: 'grid', + install(host) { + Object.assign(host.methods, Methods) + } +} diff --git a/packages/vue/src/grid/src/column-anchor/src/methods.ts b/packages/vue/src/grid/src/column-anchor/src/methods.ts new file mode 100644 index 000000000..bffe6c071 --- /dev/null +++ b/packages/vue/src/grid/src/column-anchor/src/methods.ts @@ -0,0 +1,104 @@ +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 + }, + 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]) + }) + ) + }, + buildColumnAnchorParams() { + let { columnAnchor } = this + let visibleColumn = this.getColumns() + let anchors = [] + let getAnchor = (property, label) => { + const column = visibleColumn.find((col) => !col.type && col.property === property) + let anchorName = '' + let anchorRender = null + + if (typeof label !== 'undefined') { + if (typeof label === 'string') { + anchorName = label + } else if (Array.isArray(label) && label.length) { + anchorName = String(label[0]) + anchorRender = label[1] + } + } + + if (column) { + anchors.push({ + label: anchorName || (typeof column.title === 'string' ? column.title : ''), + field: property, + active: false, + render: anchorRender + }) + } + } + + if (Array.isArray(columnAnchor) && columnAnchor.length) { + columnAnchor.forEach((item) => { + if (typeof item === 'string') { + getAnchor(item) + } else if (Array.isArray(item) && item.length) { + getAnchor(item[0], item[1]) + } + }) + } + + this.columnAnchorParams = { anchors, action: (field, e) => this.anchorAction({ field, anchors, _vm: this, e }) } + }, + anchorAction({ field, anchors, _vm }) { + const fromAnchor = anchors.find((anchor) => anchor.active) + const toAnchor = anchors.find((anchor) => anchor.field === field) + + if (toAnchor && fromAnchor !== toAnchor) { + if (fromAnchor && fromAnchor.active) { + fromAnchor.active = false + } + + if (!toAnchor.active) { + toAnchor.active = true + _vm.columnAnchorKey = field + + _vm.$nextTick((found = false) => { + const visibleColumn = _vm.getColumns() + const column = visibleColumn.find((col) => !col.type && col.property === field) + const width = visibleColumn + .filter((col) => !col.fixed) + .map((col) => { + if (col === column) { + found = true + } + return found ? 0 : col.renderWidth + }) + .reduce((p, c) => p + c, 0) + + if (column) { + _vm.scrollTo(width) + } + }) + } + } + } +} diff --git a/packages/vue/src/grid/src/dragger/index.ts b/packages/vue/src/grid/src/dragger/index.ts new file mode 100644 index 000000000..ad8f9d35b --- /dev/null +++ b/packages/vue/src/grid/src/dragger/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/dragger/src/methods.ts b/packages/vue/src/grid/src/dragger/src/methods.ts new file mode 100644 index 000000000..500718df5 --- /dev/null +++ b/packages/vue/src/grid/src/dragger/src/methods.ts @@ -0,0 +1,54 @@ +import { createHandlerOnEnd, onEndEvent } from './rowDrop' +export default { + // 处理列拖拽 + columnDrop() { + this.$nextTick(() => { + const { plugin, onBeforeMove, filter } = this.dropConfig + this.columnSortable = plugin.create( + this.$el.querySelector('.body__wrapper>.tiny-grid__header .tiny-grid-header__row'), + { + handle: '.tiny-grid-header__column:not(.col__fixed)', + filter, + onEnd: (event) => { + onEndEvent({ event, _this: this }) + }, + onStart: (event) => { + this.$emit('column-drop-start', event, this) + }, + onMove: (event) => { + const cancel = typeof onBeforeMove === 'function' ? onBeforeMove('column', null, event, this) : true + this.$emit('column-drop-move', event, this) + return cancel === undefined || cancel + } + } + ) + }) + }, + // 处理行拖拽 + rowDrop() { + this.$nextTick(() => { + const { plugin, onBeforeMove, filter, refresh = true, trigger } = this.dropConfig + this.rowSortable = plugin.create(this.$el.querySelector('.body__wrapper>.tiny-grid__body tbody'), { + handle: trigger || '.tiny-grid-body__row', + filter, + onEnd: createHandlerOnEnd({ _vm: this, refresh }), + onStart: (event) => { + this.$emit('row-drop-start', event, this) + }, + onMove: (event) => { + let insertRecords = this.getInsertRecords() + // 包含新增数据的表格不可再拖动行顺序 + if (insertRecords.length) return false + + let { dragged } = event + let selfRow = this.getRowNode(dragged).item + const cancel = typeof onBeforeMove === 'function' ? onBeforeMove('row', selfRow, event, this) : true + + this.$emit('row-drop-move', event, this) + + return cancel === undefined || cancel + } + }) + }) + } +} diff --git a/packages/vue/src/grid/src/table/src/utils/rowDrop.ts b/packages/vue/src/grid/src/dragger/src/rowDrop.ts similarity index 68% rename from packages/vue/src/grid/src/table/src/utils/rowDrop.ts rename to packages/vue/src/grid/src/dragger/src/rowDrop.ts index 12c131774..cb88c0e49 100644 --- a/packages/vue/src/grid/src/table/src/utils/rowDrop.ts +++ b/packages/vue/src/grid/src/dragger/src/rowDrop.ts @@ -24,7 +24,7 @@ */ import { findTree } from '@opentiny/vue-renderless/grid/static/' import Modal from '@opentiny/vue-modal' -import GlobalConfig from '../../../config' +import GlobalConfig from '../../config' function handleIfScrollYLoadTruthy({ isScrollYLoad, _vm, selfRow, prevTrElem, targetTrElem }) { if (!isScrollYLoad) { @@ -49,7 +49,7 @@ function handleIfScrollYLoadTruthy({ isScrollYLoad, _vm, selfRow, prevTrElem, ta targetTrElem.remove() } -function createHandlerOnEnd({ _vm, refresh }) { +export const createHandlerOnEnd = ({ _vm, refresh }) => { return (event) => { let insertRecords = _vm.getInsertRecords() // 包含新增数据的表格不可再拖动行顺序 @@ -102,4 +102,62 @@ function createHandlerOnEnd({ _vm, refresh }) { } } -export { createHandlerOnEnd } +export const getSortColumns = (columns) => { + const left = [] + const right = [] + const center = [] + + columns.forEach((col) => { + const fixed = col.fixed + + if (fixed === 'left') { + left.push(col) + } else if (fixed === 'right') { + right.push(col) + } else { + center.push(col) + } + }) + + return left.concat(center).concat(right) +} + +export const onEndEvent = ({ event, _this }) => { + const { item, newIndex, oldIndex } = event + let { fullColumn, tableColumn } = _this.getTableColumn() + const sortVisibleCols = getSortColumns(tableColumn) + let targetThElem = item + let wrapperElem = targetThElem.parentNode + let newColumn = sortVisibleCols[newIndex] + + if (newColumn.fixed) { + // 错误的移动 + if (newIndex > oldIndex) { + for (let i = newIndex; i >= oldIndex; i--) { + wrapperElem.insertBefore(targetThElem, wrapperElem.children[i]) + } + } else { + for (let i = newIndex; i <= oldIndex; i++) { + wrapperElem.insertBefore(targetThElem, wrapperElem.children[i]) + } + + wrapperElem.insertBefore(wrapperElem.children[oldIndex], targetThElem) + } + + return Modal.message({ + message: GlobalConfig.i18n('ui.grid.error.dargFixed'), + status: 'error' + }) + } + // 转换真实索引 + let oldColumnIndex = _this.getColumnIndex(sortVisibleCols[oldIndex]) + let newColumnIndex = _this.getColumnIndex(sortVisibleCols[newIndex]) + // 移动到目标列 + let currCol = fullColumn.splice(oldColumnIndex, 1)[0] + + fullColumn.splice(newColumnIndex, 0, currCol) + _this.loadColumn(fullColumn) + _this.$emit('column-drop-end', event, _this) + + _this.isDragHeaderSorting && _this.$grid.toolBarVm && _this.$grid.toolBarVm.updateSetting() +} diff --git a/packages/vue/src/grid/src/fetch-data/index.ts b/packages/vue/src/grid/src/fetch-data/index.ts new file mode 100644 index 000000000..3a765dc52 --- /dev/null +++ b/packages/vue/src/grid/src/fetch-data/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: 'grid', + install(host) { + Object.assign(host.methods, Methods) + } +} diff --git a/packages/vue/src/grid/src/fetch-data/src/methods.ts b/packages/vue/src/grid/src/fetch-data/src/methods.ts new file mode 100644 index 000000000..6c3b0f713 --- /dev/null +++ b/packages/vue/src/grid/src/fetch-data/src/methods.ts @@ -0,0 +1,106 @@ +import { getObj } from '@opentiny/vue-renderless/common/object' +import { getDataset } from '@opentiny/vue-renderless/common/dataset' +import { error } from '../../tools' +export default { + /** + * 初始化fetch-data配置项 + * @returns {object} + */ + initFetchOption() { + const { fetchData = {}, dataset = {} } = this as any + + if (fetchData.api || dataset.source || dataset.value || dataset.api) { + let { loading, fields, api } = fetchData || dataset.source || dataset.api || {} + + return { api, dataset, fields, loading } + } + }, + handleFetch(code, sortArg) { + let { pager, sortData, filterData, pagerConfig, fetchOption, fetchData, dataset } = this as any + + if (code !== 'prefetch') { + this.clearRadioRow() + this.resetScrollTop() + } + + 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, + sortBy: sortByData, + filters: filterData, + ...args + } + let search + this.tableLoading = loading + + if (pagerConfig) { + params.page = pagerConfig + } + + if (code === 'reload') { + if (pager || args.page) { + pagerConfig.currentPage = 1 + } + this.sortData = params.sort = {} + this.filterData = params.filters = [] + this.pendingRecords = [] + this.clearAll() + } + + if (sortArg && sortArg.length > 0) { + params.sortBy = sortArg + } + + if (fetchData && fetchData.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 + }) + }, + loadFetchData(rest) { + if (!rest) { + this.tableData = [] + this.tableLoading = false + return + } + + let { + fetchOption: { fields = {} }, + pagerConfig, + pagerSlot + } = this as any + + if (pagerConfig && !Array.isArray(rest)) { + let total = getObj(rest, fields.total || 'page.total') || rest?.result?.length || 0 + let data = getObj(rest, fields.result || fields.data || 'result') || [] + + this.tableData = data + pagerConfig.total = total + // 内置pager + let setTotal = pagerSlot && pagerSlot.componentInstance.setTotal + + setTotal && setTotal(total) + } else { + this.tableData = (fields.list ? getObj(rest, fields.list) : rest) || [] + } + + if ((this.seqSerial || this.scrollLoad) && pagerConfig) { + this.seqIndex = (pagerConfig.currentPage - 1) * pagerConfig.pageSize + } + + this.tableLoading = false + } +} diff --git a/packages/vue/src/grid/src/table/src/utils/handleLocalFilter.ts b/packages/vue/src/grid/src/filter/src/handleLocalFilter.ts similarity index 98% rename from packages/vue/src/grid/src/table/src/utils/handleLocalFilter.ts rename to packages/vue/src/grid/src/filter/src/handleLocalFilter.ts index 35e6a0038..11238f464 100644 --- a/packages/vue/src/grid/src/table/src/utils/handleLocalFilter.ts +++ b/packages/vue/src/grid/src/filter/src/handleLocalFilter.ts @@ -111,10 +111,10 @@ export function handleFilterCheckStr({ column, relationMethod, relations, row }) value = modifyValueCheckStr(value) switch (relation) { case 'equals': - result = value == input + result = value === input break case 'unequal': - result = value != input + result = value !== input break case 'greaterThan': result = value > input diff --git a/packages/vue/src/grid/src/filter/src/methods.ts b/packages/vue/src/grid/src/filter/src/methods.ts index d15cb1a6f..7230881b5 100644 --- a/packages/vue/src/grid/src/filter/src/methods.ts +++ b/packages/vue/src/grid/src/filter/src/methods.ts @@ -26,6 +26,13 @@ import { isArray, isBoolean } from '@opentiny/vue-renderless/grid/static/' import { getFilters, emitEvent } from '@opentiny/vue-renderless/grid/utils' import { getDataset } from '@opentiny/vue-renderless/common/dataset' import { hooks } from '@opentiny/vue-common' +import { + handleFilterConditionCustom, + handleFilterConditionExtend, + handleFilterRelations, + handleFilterCheckStr, + handleFilterCheck +} from './handleLocalFilter' function getClassName(elem) { if (elem && elem.nodeType) { @@ -56,7 +63,9 @@ function closest(elem, parentClassName) { if (elem === document.body) { break } - } while ((elem = elem.parentNode)) + + elem = elem.parentNode + } while (elem) } return null @@ -114,6 +123,36 @@ export default { return Promise.resolve(filters) }, + // 关闭筛选 + closeFilter() { + let { filterStore } = this + Object.assign(filterStore, { + visible: false, + targetElem: null, + targetElemParentTr: null + }) + return this.$nextTick() + }, + handleLocalFilter(row, column) { + let { property } = column + let { + filter: { condition, method, inputFilter } + } = column + let ret = handleFilterConditionCustom({ column, condition, method, property, row }) + if (ret.flag) { + return ret.result + } + ret = handleFilterConditionExtend({ column, condition, property, row }) + if (ret.flag) { + return ret.result + } + let { empty, input, relation, value, dateList } = condition + let { method: relationMethod } = condition + let relations = handleFilterRelations({ inputFilter }) + let checkStr = handleFilterCheckStr({ column, relationMethod, relations, row }) + let check = handleFilterCheck({ checkStr, empty, input, property, relation, row, valueList: value, dateList }) + return check() + }, getOptions({ property, filter }) { const { values, value = 'value', label = 'label', dataset } = filter diff --git a/packages/vue/src/grid/src/grid/grid.ts b/packages/vue/src/grid/src/grid/grid.ts index 8a46d692c..094f162c1 100644 --- a/packages/vue/src/grid/src/grid/grid.ts +++ b/packages/vue/src/grid/src/grid/grid.ts @@ -23,99 +23,16 @@ * */ -import { getObj } from '@opentiny/vue-renderless/common/object' -import { isBoolean, slice } from '@opentiny/vue-renderless/grid/static/' -import { removeClass, addClass } from '@opentiny/vue-renderless/common/deps/dom' -import { getDataset } from '@opentiny/vue-renderless/common/dataset' +import { isBoolean } from '@opentiny/vue-renderless/grid/static/' import { getListeners, emitEvent } from '@opentiny/vue-renderless/grid/utils' import { extend } from '@opentiny/vue-renderless/common/object' -import { h, hooks, emitter, $prefix, $props, setup, defineComponent, resolveMode } from '@opentiny/vue-common' -import Modal from '@opentiny/vue-modal' -import Pager from '@opentiny/vue-pager' -import { Buttons } from '../adapter' -import { error } from '../tools' +import { h, emitter, $prefix, $props, setup, defineComponent, resolveMode } from '@opentiny/vue-common' import TinyGridTable from '../table' import GlobalConfig from '../config' -import methods, { setBodyRecords, invokeSaveDataApi, doRemoveOrShowMsg } from './methods' -import { iconMarkOn } from '@opentiny/vue-icon' import debounce from '@opentiny/vue-renderless/common/deps/debounce' const propKeys = Object.keys(TinyGridTable.props) -// 表格工具栏渲染器 -function 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') - }) - } - - return res - })()) -} - -// 表格内置分页渲染器 -function renderPager({ $slots, _vm, loading, pager, pagerConfig, tableLoading, vSize }) { - let res = null - if ($slots.pager) { - res = $slots.pager() - } else if (pager) { - pager.component = pager.component || Pager - res = h(hooks.toRaw(pager.component), { - props: { - size: vSize, - loading: loading || tableLoading, - isBeforePageChange: _vm.isBeforePageChange || _vm.showSaveMsg, - accurateJumper: _vm.autoLoad, - ...pagerConfig - }, - on: { - 'size-change': _vm.pageSizeChange, - 'current-change': _vm.pageCurrentChange, - 'before-page-change': _vm.beforePageChangeHandler - }, - ref: 'pager', - class: _vm.viewCls('pager') - }) - } - - return res -} - -function 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 - }, - 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]) - }) - ) -} - // 渲染主入口,创建表格最外层节点 function createRender(opt) { const { @@ -148,10 +65,10 @@ function createRender(opt) { }, [ selectToolbar ? null : renderedToolbar, - columnAnchor ? renderColumnAnchor(columnAnchorParams, _vm) : null, + columnAnchor ? _vm.renderColumnAnchor(columnAnchorParams, _vm) : null, // 这里会渲染tiny-grid-column插槽内容,从而获取列配置 h(TinyGridTable, { props, on: tableOns, ref: 'tinyTable' }, $slots.default && $slots.default()), - renderPager({ + _vm.renderPager({ $slots, _vm, loading, @@ -204,6 +121,7 @@ export default defineComponent({ filterData: [], listeners: {}, pagerConfig: null, + // 存放标记为删除的行数据 pendingRecords: [], seqIndex: this.startIndex, sortData: {}, @@ -222,16 +140,18 @@ export default defineComponent({ } }, computed: { + // 工具栏按钮保存和删除时是否弹出提示信息 isMsg() { return this.proxyOpts.message !== false }, tableProps() { - let rest = {} + const rest = {} // 这里收集table组件的props,然后提供给下层组件使用 propKeys.forEach((key) => (rest[key] = this[key])) return rest }, proxyOpts() { + // 此处需要深拷贝,不然会影响全局配置 return extend(true, {}, GlobalConfig.grid.proxyConfig, this.proxyConfig) }, vSize() { @@ -348,7 +268,7 @@ export default defineComponent({ remoteFilter, remoteSort, selectToolbar - } = this + } = this as any const { seqIndex, slots: $slots, @@ -361,7 +281,7 @@ export default defineComponent({ vSize, designConfig, viewType - } = this + } = this as any const { columnAnchor, columnAnchorParams } = this // grid全局替换smb图标 @@ -370,34 +290,42 @@ export default defineComponent({ } // 初始化虚拟滚动优化配置 - let optimizOpt = { ...GlobalConfig.optimization, ...optimization } - let props = { ...tableProps, optimization: optimizOpt, startIndex: seqIndex } + const optimizOpt = { ...GlobalConfig.optimization, ...optimization } + const props = { ...tableProps, optimization: optimizOpt, startIndex: seqIndex } + // 在用户没有配置stripe时读取design配置 if (designConfig?.stripe !== undefined && !props.stripe) { // aurora规范默认带斑马条纹 props.stripe = designConfig?.stripe } - let tableOns = { ...listeners, ...tableListeners } - let { handleRowClassName: rowClassName, sortChangeEvent, filterChangeEvent } = this + const tableOns = { ...listeners, ...tableListeners } + const { handleRowClassName: rowClassName, sortChangeEvent, filterChangeEvent } = this // fetchApi状态下初始化 loading、remoteSort、remoteFilter - fetchOption && Object.assign(props, { loading: loading || tableLoading, data: tableData, rowClassName }) - fetchOption && remoteSort && (tableOns['sort-change'] = sortChangeEvent) - fetchOption && remoteFilter && (tableOns['filter-change'] = filterChangeEvent) + if (fetchOption) { + Object.assign(props, { loading: loading || tableLoading, data: tableData, rowClassName }) + remoteSort && (tableOns['sort-change'] = sortChangeEvent) + remoteFilter && (tableOns['filter-change'] = filterChangeEvent) + } // 处理表格工具栏和个性化数据 toolbar && !(toolbar.setting && toolbar.setting.storage) && (props.customs = tableCustoms) toolbar && (tableOns['update:customs'] = (value) => (this.tableCustoms = value)) - // 初始化表格编辑配置 - let editConfigOpt = { trigger: 'click', mode: 'cell', showStatus: true, ...editConfig } - // 这里handleActiveMethod处理一些编辑器的声明周期的拦截,用户传递过来的activeMethod优先级最高 - editConfig && (props.editConfig = Object.assign(editConfigOpt, { activeMethod: this.handleActiveMethod })) + if (editConfig) { + props.editConfig = { + trigger: 'click', + mode: 'cell', + showStatus: true, + ...editConfig, + activeMethod: this.handleActiveMethod + } + } // 获取工具栏的渲染器 - let renderedToolbar = getRenderedToolbar({ $slots, _vm: this, loading, tableLoading, toolbar }) + const renderedToolbar = this.getRenderedToolbar({ $slots, _vm: this, loading, tableLoading, toolbar }) // 创建表格最外层容器,并加载table组件 return createRender({ @@ -419,41 +347,6 @@ export default defineComponent({ }) }, methods: { - ...methods, - initPagerConfig() { - let { $slots, fetchOption, scrollLoad = {} } = this - let pagerProps = {} - - if (fetchOption) { - let pagerSlot = $slots.pager && $slots.pager[0] - - if (pagerSlot) { - let { componentOptions, children } = pagerSlot - - if (componentOptions && !children) { - this.pagerSlot = pagerSlot - pagerProps = componentOptions.propsData - } - } else if (this.pager) { - pagerProps = this.pager.attrs - } - - if (this.pager || $slots.pager || this.scrollLoad) { - return Object.assign(this.tablePage, { pageSize: scrollLoad.pageSize }, pagerProps) - } - - return fetchOption.args && fetchOption.args.page - } - }, - initFetchOption() { - let { fetchData = {}, dataset = {} } = this - - if (fetchData.api || dataset.source || dataset.value || dataset.api) { - let { loading, fields, api } = fetchData || dataset.source || dataset.api || {} - - return { api, dataset, fields, loading } - } - }, updateParentHeight() { if (!this.tasks.updateParentHeight) { this.tasks.updateParentHeight = debounce(10, () => { @@ -487,345 +380,6 @@ export default defineComponent({ (!this.editConfig.activeMethod || this.editConfig.activeMethod(params)) ) }, - handleFetch(code, sortArg) { - let { pager, sortData, filterData, pagerConfig, fetchOption, fetchData, dataset } = this - - if (code !== 'prefetch') { - this.clearRadioRow() - this.resetScrollTop() - } - - 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, - sortBy: sortByData, - filters: filterData, - ...args - } - let search - this.tableLoading = loading - - if (pagerConfig) { - params.page = pagerConfig - } - - if (code === 'reload') { - if (pager || args.page) { - pagerConfig.currentPage = 1 - } - this.sortData = params.sort = {} - this.filterData = params.filters = [] - this.pendingRecords = [] - this.clearAll() - } - - if (sortArg && sortArg.length > 0) { - params.sortBy = sortArg - } - - if (fetchData && fetchData.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 - }) - }, - loadFetchData(rest) { - if (!rest) { - this.tableData = [] - this.tableLoading = false - return - } - - let { - fetchOption: { fields = {} }, - pagerConfig, - pagerSlot - } = this - - if (pagerConfig && !Array.isArray(rest)) { - let total = getObj(rest, fields.total || 'page.total') || rest?.result?.length || 0 - let data = getObj(rest, fields.result || fields.data || 'result') || [] - - this.tableData = data - pagerConfig.total = total - // 内置pager - let setTotal = pagerSlot && pagerSlot.componentInstance.setTotal - - setTotal && setTotal(total) - } else { - this.tableData = (fields.list ? getObj(rest, fields.list) : rest) || [] - } - - if ((this.seqSerial || this.scrollLoad) && pagerConfig) { - this.seqIndex = (pagerConfig.currentPage - 1) * pagerConfig.pageSize - } - - this.tableLoading = false - }, - handleSave(code, args) { - let { saveData, isMsg } = this - - if (!saveData) { - error('ui.grid.error.notSave') - return - } - - let body = extend(true, { pendingRecords: this.pendingRecords }, this.getRecordset()) - let { insertRecords, removeRecords, updateRecords, pendingRecords } = body - let validRows = insertRecords.concat(updateRecords) - let getCallback = (resolve) => (valid) => { - if (!valid) { - resolve(valid) - return - } - - let canInvoke = invokeSaveDataApi({ - _vm: this, - args, - body, - code, - removeRecords, - resolve, - saveData, - updateRecords, - valid - }) - - doRemoveOrShowMsg({ _vm: this, canInvoke, code, isMsg, pendingRecords, resolve, valid }) - } - - // 排除掉新增且标记为删除的数据,排除已标记为删除的数据 - setBodyRecords({ body, insertRecords, pendingRecords }) - - // 只校验新增和修改的数据 - return new Promise((resolve) => { - this.validate(validRows, getCallback(resolve)) - }) - }, - handleDelete(code, args) { - let { deleteData, isMsg } = this - - if (!deleteData) { - error('ui.grid.error.notDelete') - return - } - - let selecteds = this.getSelectRecords(true) - let afterRemove = () => { - let removeds = this.getRemoveRecords() - - if (!removeds.length && isMsg && !selecteds.length) { - Modal.message({ - id: code, - message: GlobalConfig.i18n('ui.grid.selectOneRecord'), - status: 'warning' - }) - } - - if (removeds.length) { - let apiArgs = [{ $grid: this, changeRecords: { removeRecords: removeds } }, ...args] - let stopLoading = () => { - this.tableLoading = false - } - - this.tableLoading = true - - return deleteData.api - .apply(this, apiArgs) - .then(stopLoading) - .catch(stopLoading) - .then(() => this.commitProxy('reload')) - } - } - - this.remove(selecteds).then(afterRemove) - }, - handleFullScreen([show]) { - const cls = 'tiny-fullscreen-full' - - show ? addClass(this.$el, cls) : removeClass(this.$el, cls) - - this.recalculate() - - emitEvent(this, 'fullscreen', show) - this.emitter.emit('fullscreen', show) - }, - commitProxy(code) { - let btnMethod = Buttons.get(code) - let args = slice(arguments, 1) - - 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') { - this.handleDeleteRow(code, 'ui.grid.deleteSelectRecord', () => - this.commitProxy.apply(this, ['delete'].concat(args)) - ) - } else if (code === 'remove_selection') { - this.handleDeleteRow(code, 'ui.grid.removeSelectRecord', () => this.removeSelecteds()) - } else if (code === 'export') { - this.exportCsv() - } else if (code === 'reset_custom') { - this.resetAll() - } else if (~['reload', 'query', 'prefetch'].indexOf(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) - } - - return this.$nextTick() - }, - handleDeleteRow(code, i18nKey, callback) { - let selecteds = this.getSelectRecords() - - if (this.isMsg && selecteds.length) { - Modal.confirm(GlobalConfig.i18n(i18nKey)).then((type) => { - type === 'confirm' && callback() - }) - } - - if (this.isMsg && !selecteds.length) { - Modal.message({ - id: code, - message: GlobalConfig.i18n('ui.grid.selectOneRecord'), - status: 'warning' - }) - } - - if (!this.isMsg && selecteds.length) { - callback() - } - }, - getPendingRecords() { - return this.pendingRecords - }, - triggerToolbarBtnEvent(button, event) { - let { events = {}, tableListeners } = this - let { code } = button - - if (!events.toolbarButtonClick && !tableListeners['toolbar-button-click']) { - this.commitProxy(code, event) - } - - emitEvent(this, 'toolbar-button-click', [{ code, button, $grid: this }, event]) - - this.emitter.emit('toolbar-button-click', { code, button, $grid: this }, event) - }, - triggerPendingEvent(code) { - let { isMsg, pendingRecords: pendings } = this - let selectColumn = this.getColumns().filter((col) => ~['selection', 'radio'].indexOf(col.type)) - let isSelection = selectColumn.length && selectColumn[0].type === 'selection' - let isRadio = selectColumn.length && selectColumn[0].type === 'radio' - let selecteds = isSelection ? this.getSelectRecords(true) : isRadio ? [this.getRadioRow()] : [] - - if (!selecteds.length && isMsg) { - Modal.message({ - id: code, - message: GlobalConfig.i18n('ui.grid.selectOneRecord'), - status: 'warning' - }) - } - - if (selecteds.length) { - let { plus = [], minus = [], tmp } = {} - - selecteds.forEach((data) => { - let selectedPending = pendings.includes(data) - - tmp = selectedPending ? minus : plus - tmp.push(data) - }) - - tmp = minus.length ? pendings.filter((item) => !~minus.indexOf(item)) : pendings - this.pendingRecords = tmp.concat(plus) - - isSelection && this.clearSelection() - isRadio && this.clearRadioRow() - } - }, - pageChangeEvent(params) { - // 这里需要做下防抖操作,防止在pageSize从小变大的时候导致fetch-data触发多次 - if (!this.tasks.updatePage) { - this.tasks.updatePage = debounce(200, () => { - const eventParams = { $grid: this, ...params } - - // 处理标签式监听事件的:@page-change - emitEvent(this, 'page-change', eventParams) - - // 处理配置式表格的监听事件 - this.emitter.emit('page-change', eventParams) - - // 触发fetchData - this.commitProxy('query') - - if (this.toolBarVm) { - this.toolBarVm.orderSetting() - } - }) - } - this.tasks.updatePage() - }, - // size为页大小,load为false则触发change事件与查询,在个性化初始化时根据autoload控制是否加载数据 - pageSizeChange(size, load) { - this.tablePage.pageSize = size - this.tablePage.currentPage = 1 - load || this.pageChangeEvent(this.tablePage) - }, - pageCurrentChange(current) { - this.tablePage.currentPage = current - this.pageChangeEvent(this.tablePage) - }, - beforePageChangeHandler(params) { - if (!this.showSaveMsg) { - let eventParams = extend(false, { $grid: this }, params) - - emitEvent(this, 'before-page-change', eventParams) - this.emitter.emit('before-page-change', eventParams) - - return - } - - let { callback, rollback } = params - let { insertRecords, removeRecords, updateRecords } = this.getRecordset() - - if (insertRecords.length || removeRecords.length || updateRecords.length) { - let next = (res) => { - if (res === 'confirm') { - rollback && rollback() - emitEvent(this, 'cancel-page-change', this) - this.emitter.emit('cancel-page-change', this) - } else { - callback && callback() - } - } - - Modal.confirm(GlobalConfig.i18n('ui.grid.isSaveMsg')).then(next) - } else { - callback && callback() - } - }, sortChangeEvent(params) { let remoteSort = this.remoteSort let column = params.column @@ -843,74 +397,7 @@ export default defineComponent({ viewCls(module) { return GlobalConfig.viewConfig[module][this.viewType] || '' }, - buildColumnAnchorParams() { - let { columnAnchor } = this - let visibleColumn = this.getColumns() - let anchors = [] - let getAnchor = (property, label) => { - const column = visibleColumn.find((col) => !col.type && col.property === property) - let anchorName = '' - let anchorRender = null - - if (typeof label !== 'undefined') { - if (typeof label === 'string') { - anchorName = label - } else if (Array.isArray(label) && label.length) { - anchorName = String(label[0]) - anchorRender = label[1] - } - } - - if (column) { - anchors.push({ - label: anchorName || (typeof column.title === 'string' ? column.title : ''), - field: property, - active: false, - render: anchorRender - }) - } - } - - if (Array.isArray(columnAnchor) && columnAnchor.length) { - columnAnchor.map((item) => { - if (typeof item === 'string') { - getAnchor(item) - } else if (Array.isArray(item) && item.length) { - getAnchor(item[0], item[1]) - } - }) - } - - this.columnAnchorParams = { anchors, action: (field, e) => this.anchorAction({ field, anchors, _vm: this, e }) } - }, - anchorAction({ field, anchors, _vm }) { - const fromAnchor = anchors.find((anchor) => anchor.active) - const toAnchor = anchors.find((anchor) => anchor.field === field) - - if (toAnchor && fromAnchor !== toAnchor) { - if (fromAnchor && fromAnchor.active) { - fromAnchor.active = false - } - - if (!toAnchor.active) { - toAnchor.active = true - _vm.columnAnchorKey = field - - _vm.$nextTick((found = false) => { - const visibleColumn = _vm.getColumns() - const column = visibleColumn.find((col) => !col.type && col.property === field) - const width = visibleColumn - .filter((col) => !col.fixed) - .map((col) => (col === column && (found = true), found ? 0 : col.renderWidth)) - .reduce((p, c) => p + c, 0) - - if (column) { - _vm.scrollTo(width) - } - }) - } - } - }, + // 监听某个元素是否出现在视口中 addIntersectionObserver() { if (this.intersectionOption && this.intersectionOption.disabled) return diff --git a/packages/vue/src/grid/src/grid/methods.ts b/packages/vue/src/grid/src/grid/methods.ts deleted file mode 100644 index d3751df0d..000000000 --- a/packages/vue/src/grid/src/grid/methods.ts +++ /dev/null @@ -1,92 +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 Modal from '@opentiny/vue-modal' -import GlobalConfig from '../config' -import Table from '../table' - -export function setBodyRecords({ body, insertRecords, pendingRecords }) { - if (insertRecords.length) { - body.pendingRecords = pendingRecords.filter((row) => !insertRecords.includes(row)) - } - - if (pendingRecords.length) { - body.insertRecords = insertRecords.filter((row) => !pendingRecords.includes(row)) - } -} - -function canInvokeSaveDataApi(body, removeRecords, updateRecords) { - return body.insertRecords.length || removeRecords.length || updateRecords.length || body.pendingRecords.length -} - -export function doRemoveOrShowMsg({ _vm, canInvoke, code, isMsg, pendingRecords, resolve, valid }) { - if (valid && !canInvoke) { - if (isMsg) { - // 直接移除未保存且标记为删除的数据 - if (pendingRecords.length) { - _vm.remove(pendingRecords) - } else { - Modal.message({ - id: code, - message: GlobalConfig.i18n('ui.grid.dataUnchanged'), - status: 'info' - }) - } - } - - resolve() - } -} - -export function invokeSaveDataApi({ _vm, args, body, code, removeRecords, resolve, saveData, updateRecords, valid }) { - let canInvoke = false - - if (valid && (canInvoke = canInvokeSaveDataApi(body, removeRecords, updateRecords))) { - _vm.tableLoading = true - - resolve( - saveData.api - .apply(_vm, [{ $grid: _vm, changeRecords: body }].concat(args)) - .then(() => { - Modal.message({ - id: code, - message: GlobalConfig.i18n('ui.grid.saveSuccess'), - status: 'success' - }) - _vm.tableLoading = false - }) - .catch(() => { - _vm.tableLoading = false - }) - .then(() => _vm.commitProxy('reload')) - ) - } - - return canInvoke -} - -const getWrapFunc = (name) => - function () { - const tinyTable = this.$refs.tinyTable - if (tinyTable) { - return this.$refs.tinyTable[name].apply(tinyTable, arguments) - } - } - -const methods = {} -const methodNames = Object.keys(Table.methods).concat(['exportCsv', 'clearFilter', 'exportExcel']) - -// 将table组件的方法,传递给grid组件使用,this指向全部指向tinyTable -methodNames.forEach((name) => { - methods[name] = getWrapFunc(name) -}) - -export default methods diff --git a/packages/vue/src/grid/src/pager/index.ts b/packages/vue/src/grid/src/pager/index.ts new file mode 100644 index 000000000..3a765dc52 --- /dev/null +++ b/packages/vue/src/grid/src/pager/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: 'grid', + install(host) { + Object.assign(host.methods, Methods) + } +} diff --git a/packages/vue/src/grid/src/pager/src/methods.ts b/packages/vue/src/grid/src/pager/src/methods.ts new file mode 100644 index 000000000..556e0f061 --- /dev/null +++ b/packages/vue/src/grid/src/pager/src/methods.ts @@ -0,0 +1,124 @@ +import debounce from '@opentiny/vue-renderless/common/deps/debounce' +import { emitEvent } from '@opentiny/vue-renderless/grid/utils' +import Modal from '@opentiny/vue-modal' +import Pager from '@opentiny/vue-pager' +import { extend } from '@opentiny/vue-renderless/common/object' +import { h, hooks } from '@opentiny/vue-common' +import GlobalConfig from '../../config' + +export default { + // 初始化表格分页配置 + initPagerConfig() { + let { $slots, fetchOption, scrollLoad = {} } = this as any + let pagerProps = {} + + if (fetchOption) { + let pagerSlot = $slots.pager && $slots.pager[0] + + if (pagerSlot) { + let { componentOptions, children } = pagerSlot + + if (componentOptions && !children) { + this.pagerSlot = pagerSlot + pagerProps = componentOptions.propsData + } + } else if (this.pager) { + pagerProps = this.pager.attrs + } + + if (this.pager || $slots.pager || this.scrollLoad) { + return Object.assign(this.tablePage, { pageSize: scrollLoad.pageSize }, pagerProps) + } + + return fetchOption.args && fetchOption.args.page + } + }, + // 表格内置分页渲染器 + renderPager({ $slots, _vm, loading, pager, pagerConfig, tableLoading, vSize }) { + let res = null + if ($slots.pager) { + res = $slots.pager() + } else if (pager) { + pager.component = pager.component || Pager + res = h(hooks.toRaw(pager.component), { + props: { + size: vSize, + loading: loading || tableLoading, + isBeforePageChange: _vm.isBeforePageChange || _vm.showSaveMsg, + accurateJumper: _vm.autoLoad, + ...pagerConfig + }, + on: { + 'size-change': _vm.pageSizeChange, + 'current-change': _vm.pageCurrentChange, + 'before-page-change': _vm.beforePageChangeHandler + }, + ref: 'pager', + class: _vm.viewCls('pager') + }) + } + + return res + }, + pageChangeEvent(params) { + // 这里需要做下防抖操作,防止在pageSize从小变大的时候导致fetch-data触发多次 + if (!this.tasks.updatePage) { + this.tasks.updatePage = debounce(200, () => { + const eventParams = { $grid: this, ...params } + + // 处理标签式监听事件的:@page-change + emitEvent(this, 'page-change', eventParams) + + // 处理配置式表格的监听事件 + this.emitter.emit('page-change', eventParams) + + // 触发fetchData + this.commitProxy('query') + + if (this.toolBarVm) { + this.toolBarVm.orderSetting() + } + }) + } + this.tasks.updatePage() + }, + // size为页大小,load为false则触发change事件与查询,在个性化初始化时根据autoload控制是否加载数据 + pageSizeChange(size, load) { + this.tablePage.pageSize = size + this.tablePage.currentPage = 1 + load || this.pageChangeEvent(this.tablePage) + }, + pageCurrentChange(current) { + this.tablePage.currentPage = current + this.pageChangeEvent(this.tablePage) + }, + beforePageChangeHandler(params) { + if (!this.showSaveMsg) { + let eventParams = extend(false, { $grid: this }, params) + + emitEvent(this, 'before-page-change', eventParams) + this.emitter.emit('before-page-change', eventParams) + + return + } + + let { callback, rollback } = params + let { insertRecords, removeRecords, updateRecords } = this.getRecordset() + + if (insertRecords.length || removeRecords.length || updateRecords.length) { + let next = (res) => { + if (res === 'confirm') { + rollback && rollback() + emitEvent(this, 'cancel-page-change', this) + this.emitter.emit('cancel-page-change', this) + } else { + callback && callback() + } + } + + Modal.confirm(GlobalConfig.i18n('ui.grid.isSaveMsg')).then(next) + } else { + callback && callback() + } + } +} diff --git a/packages/vue/src/grid/src/sort/index.ts b/packages/vue/src/grid/src/sort/index.ts new file mode 100644 index 000000000..ad8f9d35b --- /dev/null +++ b/packages/vue/src/grid/src/sort/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/sort/src/methods.ts b/packages/vue/src/grid/src/sort/src/methods.ts new file mode 100644 index 000000000..b1c6ea436 --- /dev/null +++ b/packages/vue/src/grid/src/sort/src/methods.ts @@ -0,0 +1 @@ +export default {} diff --git a/packages/vue/src/grid/src/table/src/events.ts b/packages/vue/src/grid/src/table/src/events.ts index 052127775..13958b18e 100644 --- a/packages/vue/src/grid/src/table/src/events.ts +++ b/packages/vue/src/grid/src/table/src/events.ts @@ -242,40 +242,3 @@ export function handleGlobalKeydownEvent(event) { export function handleGlobalResizeEvent() { this.recalculate() } - -// 触发表头 tooltip 事件 -export function triggerHeaderTooltipEvent(event, params) { - let { tooltipStore } = this - let { column, showHeaderTip } = params - if (tooltipStore.column !== column || !tooltipStore.visible) { - // 在 v3.0 中废弃 label - this.handleTooltip(event, column, null, showHeaderTip, true) - } -} - -// 触发表尾 tooltip 事件 -export function triggerFooterTooltipEvent(event, params) { - let { column } = params - let tooltipStore = this.tooltipStore - if (tooltipStore.column !== column || !tooltipStore.visible) { - this.handleTooltip(event, column) - } -} - -// 触发 tooltip 事件 -export function triggerTooltipEvent(event, params) { - let { editConfig, editStore, tooltipStore } = this - let { actived } = editStore - let { row, column, showTip } = params - if (editConfig) { - if ( - (editConfig.mode === 'row' && actived.row === row && column.editor) || - (actived.row === row && actived.column === column) - ) { - return - } - } - if (tooltipStore.column !== column || tooltipStore.row !== row || !tooltipStore.visible) { - this.handleTooltip(event, column, row, showTip) - } -} diff --git a/packages/vue/src/grid/src/table/src/funcs.ts b/packages/vue/src/grid/src/table/src/funcs.ts index d0801a58b..2a681117f 100644 --- a/packages/vue/src/grid/src/table/src/funcs.ts +++ b/packages/vue/src/grid/src/table/src/funcs.ts @@ -9,8 +9,6 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ -import Modal from '@opentiny/vue-modal' -import GlobalConfig from '../../config' import { warn } from '../../tools' import { isArray, get } from '@opentiny/vue-renderless/grid/static/' import { preprocessDataObjectFormat, preventDupRender, handleResolveColumnComplete } from './utils/handleResolveColumn' @@ -53,66 +51,6 @@ export const headerProps = { children: 'children' } -export const getSortColumns = (columns) => { - const left = [] - const right = [] - const center = [] - - columns.forEach((col) => { - const fixed = col.fixed - - if (fixed === 'left') { - left.push(col) - } else if (fixed === 'right') { - right.push(col) - } else { - center.push(col) - } - }) - - return left.concat(center).concat(right) -} - -export const onEndEvent = ({ event, _this }) => { - const { item, newIndex, oldIndex } = event - let { fullColumn, tableColumn } = _this.getTableColumn() - const sortVisibleCols = getSortColumns(tableColumn) - let targetThElem = item - let wrapperElem = targetThElem.parentNode - let newColumn = sortVisibleCols[newIndex] - - if (newColumn.fixed) { - // 错误的移动 - if (newIndex > oldIndex) { - for (let i = newIndex; i >= oldIndex; i--) { - wrapperElem.insertBefore(targetThElem, wrapperElem.children[i]) - } - } else { - for (let i = newIndex; i <= oldIndex; i++) { - wrapperElem.insertBefore(targetThElem, wrapperElem.children[i]) - } - - wrapperElem.insertBefore(wrapperElem.children[oldIndex], targetThElem) - } - - return Modal.message({ - message: GlobalConfig.i18n('ui.grid.error.dargFixed'), - status: 'error' - }) - } - // 转换真实索引 - let oldColumnIndex = _this.getColumnIndex(sortVisibleCols[oldIndex]) - let newColumnIndex = _this.getColumnIndex(sortVisibleCols[newIndex]) - // 移动到目标列 - let currCol = fullColumn.splice(oldColumnIndex, 1)[0] - - fullColumn.splice(newColumnIndex, 0, currCol) - _this.loadColumn(fullColumn) - _this.$emit('column-drop-end', event, _this) - - _this.isDragHeaderSorting && _this.$grid.toolBarVm && _this.$grid.toolBarVm.updateSetting() -} - export const handleAllColumnPromises = (opt, ctx) => { let { startIndex, fetchColumns, tableData, asyncRenderMap, isScrollLoad } = opt return (data) => { diff --git a/packages/vue/src/grid/src/table/src/methods.ts b/packages/vue/src/grid/src/table/src/methods.ts index 5bae37781..eff549c56 100644 --- a/packages/vue/src/grid/src/table/src/methods.ts +++ b/packages/vue/src/grid/src/table/src/methods.ts @@ -24,13 +24,11 @@ */ import { getColumnList } from '@opentiny/vue-renderless/grid/utils' import { toDecimal } from '@opentiny/vue-renderless/common/string' -import { getStyle } from '@opentiny/vue-renderless/common/deps/dom' import { addClass, removeClass } from '@opentiny/vue-renderless/common/deps/dom' import debounce from '@opentiny/vue-renderless/common/deps/debounce' import { isNumber, filterTree, - remove, isArray, isBoolean, findTree, @@ -48,9 +46,7 @@ import { destructuring, clear, sum, - find, - toStringJSON, - toArray + find } from '@opentiny/vue-renderless/grid/static/' import browser from '@opentiny/vue-renderless/common/browser' import { @@ -69,8 +65,6 @@ import { error, warn } from '../../tools' import TINYGrid, { Interceptor } from '../../adapter' import GlobalConfig from '../../config' import { handleLayout } from './utils/updateStyle' -import { createTooltipRange, processContentMethod } from './utils/handleTooltip' -import { hasCheckField, hasNoCheckField } from './utils/handleSelectRow' import { isTargetRadioOrCheckbox, onClickExpandColumn, @@ -88,24 +82,12 @@ import { showGroupFixedError, onScrollXLoad } from './utils/refreshColumn' -import { - handleFilterConditionCustom, - handleFilterConditionExtend, - handleFilterRelations, - handleFilterCheckStr, - handleFilterCheck -} from './utils/handleLocalFilter' -import { hasCheckFieldNoStrictly, hasNoCheckFieldNoStrictly, setSelectionNoStrictly } from './utils/setAllSelection' import { mapFetchColumnPromise } from './utils/handleResolveColumn' import { computeScrollYLoad, computeScrollXLoad } from './utils/computeScrollLoad' -import { createHandlerOnEnd } from './utils/rowDrop' import { calcTableWidth, calcFixedStickyPosition } from './utils/autoCellWidth' import { generateFixedClassName } from './utils/handleFixedColumn' -import { funcs, headerProps, onEndEvent, handleAllColumnPromises } from './funcs' +import { funcs, headerProps, handleAllColumnPromises } from './funcs' import { - triggerHeaderTooltipEvent, - triggerFooterTooltipEvent, - triggerTooltipEvent, handleGlobalMousedownEvent, handleGlobalBlurEvent, handleGlobalMousewheelEvent, @@ -134,19 +116,20 @@ import { let run = (names, $table) => names.forEach((name) => $table[name].apply($table)) let isWebkit = browser['-webkit'] -let debounceScrollDirDuration = browser.name === 'ie' ? 40 : 20 +let debounceScrollDirDuration = 20 let debounceScrollLoadDuration = 200 let AsyncCollectTimeout = 100 -let focusSingle = null // 多字段排序 const sortMultiple = (rows, columns, _vm) => { const greaterThan = (valueP, valueQ) => { - let typeP - - return (typeP = typeof valueP) === typeof valueQ && ['number', 'string', 'boolean'].includes(typeP) - ? valueP > valueQ - : String(valueP) > String(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 @@ -174,57 +157,6 @@ const sortMultiple = (rows, columns, _vm) => { } const Methods = { - // 处理列拖拽 - columnDrop() { - this.$nextTick(() => { - const { plugin, onBeforeMove, filter } = this.dropConfig - this.columnSortable = plugin.create( - this.$el.querySelector('.body__wrapper>.tiny-grid__header .tiny-grid-header__row'), - { - handle: '.tiny-grid-header__column:not(.col__fixed)', - filter, - onEnd: (event) => { - onEndEvent({ event, _this: this }) - }, - onStart: (event) => { - this.$emit('column-drop-start', event, this) - }, - onMove: (event) => { - const cancel = typeof onBeforeMove === 'function' ? onBeforeMove('column', null, event, this) : true - this.$emit('column-drop-move', event, this) - return cancel === undefined || cancel - } - } - ) - }) - }, - // 处理行拖拽 - rowDrop() { - this.$nextTick(() => { - const { plugin, onBeforeMove, filter, refresh = true, trigger } = this.dropConfig - this.rowSortable = plugin.create(this.$el.querySelector('.body__wrapper>.tiny-grid__body tbody'), { - handle: trigger || '.tiny-grid-body__row', - filter, - onEnd: createHandlerOnEnd({ _vm: this, refresh }), - onStart: (event) => { - this.$emit('row-drop-start', event, this) - }, - onMove: (event) => { - let insertRecords = this.getInsertRecords() - // 包含新增数据的表格不可再拖动行顺序 - if (insertRecords.length) return false - - let { dragged } = event - let selfRow = this.getRowNode(dragged).item - const cancel = typeof onBeforeMove === 'function' ? onBeforeMove('row', selfRow, event, this) : true - - this.$emit('row-drop-move', event, this) - - return cancel === undefined || cancel - } - }) - }) - }, getParentElem() { let $el = this.$grid ? this.$grid.$el : this.$el return $el.parentNode @@ -613,11 +545,6 @@ const Methods = { collectColumn: collectColumn.slice(0) } }, - // 在v3.0中废弃getRecords - getRecords() { - warn('ui.grid.error.delGetRecords') - return this.getData.apply(this, arguments) - }, // 获取表格所有数据 getData(rowIndex) { let tableSynchData = this.data || this.tableSynchData @@ -629,11 +556,6 @@ const Methods = { } return undefined }, - // 在v3.0中废弃getAllRecords - getAllRecords() { - warn('ui.grid.error.delGetAllRecords') - return this.getRecordset() - }, // 获取选中数据。notCopy为true不返回数据副本,表格内部要继续处理其返回值时设置为true getSelectRecords(notCopy) { let { selectConfig = {}, selection } = this @@ -654,26 +576,6 @@ const Methods = { } return notCopy ? rowList : clone(rowList, true) }, - handleLocalFilter(row, column) { - let { property } = column - let { - filter: { condition, method, inputFilter } - } = column - let ret = handleFilterConditionCustom({ column, condition, method, property, row }) - if (ret.flag) { - return ret.result - } - ret = handleFilterConditionExtend({ column, condition, property, row }) - if (ret.flag) { - return ret.result - } - let { empty, input, relation, value, dateList } = condition - let { method: relationMethod } = condition - let relations = handleFilterRelations({ inputFilter }) - let checkStr = handleFilterCheckStr({ column, relationMethod, relations, row }) - let check = handleFilterCheck({ checkStr, empty, input, property, relation, row, valueList: value, dateList }) - return check() - }, // 对数据进行筛选和排序,获取处理后数据。服务端筛选和排序,在接口调用时已传入参数 updateAfterFullData() { let { remoteFilter, remoteSort, tableFullData, visibleColumn, sortOpts } = this @@ -1108,258 +1010,6 @@ const Methods = { handleOtherKeyDown, handleGlobalKeydownEvent, handleGlobalResizeEvent, - triggerHeaderTooltipEvent, - triggerFooterTooltipEvent, - triggerTooltipEvent, - // 显示 tooltip 依赖的 popper 组件 - activateTooltip(tooltip, isValid) { - if (!this.tasks.activateTooltip) { - this.tasks.activateTooltip = debounce(300, () => { - let sign = isValid !== undefined ? isValid : focusSingle - - if (sign) { - tooltip.state.popperElm && (tooltip.state.popperElm.style.display = 'none') - tooltip.doDestroy() - tooltip.show() - setTimeout(tooltip.updatePopper) - } - }) - } - - this.tasks.activateTooltip() - }, - // 显示 tooltip 依赖的 popper 组件 - activateTooltipValid(tooltip) { - if (!this.tasks.activateTooltipValid) { - this.tasks.activateTooltipValid = debounce(50, () => { - tooltip.handleShowPopper() - setTimeout(() => tooltip.updatePopper()) - }) - } - - this.tasks.activateTooltipValid() - }, - // 显示 tooltip - handleTooltip(event, column, row, showTip, isHeader) { - const cell = isHeader - ? event.currentTarget.querySelector('.tiny-grid-cell-text') - : event.currentTarget.querySelector('.tiny-grid-cell') - - // 当用户悬浮在排序或者筛选图标按钮时不应该显示tooltip - if (isHeader && event.target !== cell) { - return - } - const tooltip = this.$refs.tooltip - const wrapperElem = cell - const content = cell.innerText.trim() || cell.textContent.trim() - const { contentMethod } = this.tooltipConfig - const range = createTooltipRange({ _vm: this, cell, column, isHeader }) - const rangeWidth = range.getBoundingClientRect().width - const padding = - (parseInt(getStyle(cell, 'paddingLeft'), 10) || 0) + (parseInt(getStyle(cell, 'paddingRight'), 10) || 0) - const isOverflow = rangeWidth + padding > cell.offsetWidth || wrapperElem.scrollWidth > wrapperElem.clientWidth - - // content如果是空字符串,但是用户配置了contentMethod,则同样也可以触发提示 - if ((contentMethod || content) && (showTip || isOverflow)) { - Object.assign(this.tooltipStore, { row, column, visible: true }) - - if (tooltip) { - processContentMethod({ _vm: this, column, content, contentMethod, event, isHeader, row, showTip }) - tooltip.state.referenceElm = cell - tooltip.state.popperElm && (tooltip.state.popperElm.style.display = 'none') - - focusSingle = true - this.activateTooltip(tooltip) - } - } - - return this.$nextTick() - }, - // 提供关闭tips提示的方法 - clostTooltip() { - let tooltip = this.$refs.tooltip - Object.assign(this.tooltipStore, { - content: null, - row: null, - visible: false, - column: null - }) - focusSingle = false - if (tooltip && typeof tooltip.setExpectedState === 'function') { - tooltip.setExpectedState(false) - this.debounceClose(tooltip) - } - return this.$nextTick() - }, - // 添加代码让用户代码可以划入popper - debounceClose(tooltip) { - if (!this.tasks.debounceClose) { - this.tasks.debounceClose = debounce(50, () => { - tooltip.handleClosePopper() - }) - } - - this.tasks.debounceClose() - }, - // 处理默认勾选 - handleSelectionDefChecked() { - let fullDataRowIdData = this.fullDataRowIdData - let { checkAll, checkRowKeys } = this.selectConfig || {} - - if (checkAll) { - this.setAllSelection(true) - return - } - if (checkRowKeys) { - let defCheckedRowids = checkRowKeys.map((key) => encodeURIComponent(key)) - let defCheckedRows = [] - - defCheckedRowids.forEach((rowid) => { - let rowCache = fullDataRowIdData[rowid] - if (rowCache) { - defCheckedRows.push(rowCache.row) - } - }) - - this.setSelection(defCheckedRows, true) - } - }, - setSelection(rows, value) { - if (rows) { - if (!isArray(rows)) { - rows = [rows] - } - rows.forEach((row) => this.handleSelectRow({ row }, !!value)) - } - return this.$nextTick() - }, - // 多选,行选中事件。value:选中true、不选false、不确定-1 - handleSelectRow({ row }, value) { - hasCheckField({ row }, value, this) - hasNoCheckField({ row }, value, this) - this.checkSelectionStatus() - }, - handleToggleCheckRowEvent(params, event) { - let selection = this.selection - let { checkField } = this.selectConfig || {} - let { row } = params - let value = checkField ? !get(row, checkField) : !~selection.indexOf(row) - if (event) { - this.triggerCheckRowEvent(event, params, value) - } else { - this.handleSelectRow(params, value) - } - }, - triggerCheckRowEvent(event, params, value) { - let { selectConfig = {} } = this - let { checkMethod } = selectConfig - if (!checkMethod || checkMethod(params)) { - this.handleSelectRow(params, value) - emitEvent(this, 'select-change', [ - { - selection: this.getSelectRecords(), - checked: value, - $table: this, - ...params - }, - event - ]) - } - }, - // 多选,切换某一行的选中状态 - toggleRowSelection(row) { - this.handleToggleCheckRowEvent({ row }) - return this.$nextTick() - }, - setAllSelection(value) { - let { afterFullData, selectConfig = {}, treeConfig, selection } = this - let { checkField: property, reserve, checkStrictly, checkMethod } = selectConfig - hasCheckFieldNoStrictly({ afterFullData, checkMethod, checkStrictly, property, selection, treeConfig, value }) - let selectRows = hasNoCheckFieldNoStrictly({ - afterFullData, - checkMethod, - checkStrictly, - property, - selection, - treeConfig, - value - }) - setSelectionNoStrictly({ _vm: this, checkStrictly, reserve, selectRows, selection, value }) - this.treeIndeterminates = [] - this.checkSelectionStatus() - }, - checkSelectionStatus() { - let { afterFullData, selection, treeIndeterminates } = this - let { checkField, checkStrictly, checkMethod } = this.selectConfig || {} - let { everyHandler, someHandler } = {} - if (checkStrictly) { - return - } - // 包含新增的数据 - if (checkField) { - everyHandler = checkMethod - ? (row, rowIndex) => !checkMethod({ row, rowIndex }) || get(row, checkField) - : (row) => get(row, checkField) - someHandler = (row) => get(row, checkField) || ~treeIndeterminates.indexOf(row) - this.isAllSelected = false - afterFullData.length && (this.isAllSelected = afterFullData.every(everyHandler)) - this.isIndeterminate = !this.isAllSelected && afterFullData.some(someHandler) - } else { - everyHandler = (row, rowIndex) => !checkMethod({ row, rowIndex }) - this.headerCheckDisabled = checkMethod && afterFullData.length && afterFullData.every(everyHandler) - everyHandler = checkMethod - ? (row, rowIndex) => !checkMethod({ row, rowIndex }) || ~selection.indexOf(row) - : (row) => ~selection.indexOf(row) - someHandler = (row) => ~treeIndeterminates.indexOf(row) || ~selection.indexOf(row) - this.isAllSelected = false - afterFullData.length && (this.isAllSelected = afterFullData.every(everyHandler)) - this.isIndeterminate = !this.isAllSelected && afterFullData.some(someHandler) - } - }, - // 保留选中状态 - reserveCheckSelection() { - let { fullDataRowIdData, selection } = this - let { reserve } = this.selectConfig || {} - let rowkey = getTableRowKey(this) - if (reserve && selection.length) { - this.selection = selection.map((row) => { - let rowCache = fullDataRowIdData[`${get(row, rowkey)}`] - return rowCache ? rowCache.row : row - }) - } - }, - // 多选,选中所有事件 - triggerCheckAllEvent(event, value) { - this.setAllSelection(value) - let eventParams = { - selection: this.getSelectRecords(), - checked: value, - $table: this - } - emitEvent(this, 'select-all', [eventParams, event]) - }, - // 多选,切换所有行的选中状态 - toggleAllSelection() { - this.triggerCheckAllEvent(null, !this.isAllSelected) - return this.$nextTick() - }, - clearSelection() { - let { tableFullData, treeConfig } = this - let { checkField } = this.selectConfig || {} - if (checkField) { - treeConfig - ? eachTree(tableFullData, (item) => set(item, checkField, false), treeConfig) - : tableFullData.forEach((item) => set(item, checkField, false)) - } - Object.assign(this, { - isAllSelected: false, - isIndeterminate: false, - selection: [], - treeIndeterminates: [] - }) - - return this.$nextTick() - }, // 处理单选框默认勾选 handleRadioDefChecked() { let { fullDataRowIdData } = this @@ -1573,16 +1223,6 @@ const Methods = { return this.handleTableData(true).then(this.refreshStyle) }, - // 关闭筛选 - closeFilter() { - let { filterStore } = this - Object.assign(filterStore, { - visible: false, - targetElem: null, - targetElemParentTr: null - }) - return this.$nextTick() - }, toggleGroupExpansion(row) { this.groupExpandeds.push(row) }, @@ -1659,116 +1299,6 @@ const Methods = { this.expandeds = [] return this.$nextTick().then(() => (hasExpand ? this.recalculate() : 0)) }, - // 展开树节点事件 - triggerTreeExpandEvent(event, { row }) { - let { currentColumn, currentRow } = this - let rest = this.toggleTreeExpansion(row) - let eventParams = { $table: this, row, rowIndex: this.getRowIndex(row) } - emitEvent(this, 'toggle-tree-change', [eventParams, event]) - this.$nextTick(() => { - currentRow ? this.setCurrentRow(currentRow) : currentColumn ? this.setCurrentColumn(currentColumn) : '' - }) - return rest - }, - // 切换/展开树节点 - toggleTreeExpansion(row) { - return this.setTreeExpansion(row) - }, - // 处理默认展开树节点 - handleDefaultTreeExpand() { - let { tableFullData, treeConfig } = this - if (!treeConfig) { - return - } - let { children, expandAll, expandRowKeys: rowids } = treeConfig - let treeExpandeds = [] - let rowkey = getTableRowKey(this) - let isNonEmptyArr = (arr) => isArray(arr) && arr.length - // 展开所有行 - let doExpandAll = () => { - filterTree(tableFullData, (row) => isNonEmptyArr(row[children]) && treeExpandeds.push(row), treeConfig) - this.treeExpandeds = treeExpandeds - } - // 展开指定行 - let doExpandRows = () => { - rowids.forEach((rowid) => { - let matchObj = findTree(tableFullData, (item) => rowid === get(item, rowkey), treeConfig) - - matchObj && isNonEmptyArr(matchObj.item[children]) && treeExpandeds.push(matchObj.item) - }) - this.treeExpandeds = treeExpandeds - } - expandAll ? doExpandAll() : rowids ? doExpandRows() : '' - setTreeScrollYCache(this) - }, - setAllTreeExpansion(expanded) { - let { tableFullData, treeConfig } = this - let children = treeConfig.children - let treeExpandeds = [] - if (expanded) { - let rowHandler = (row) => { - if (row[children] && row[children].length) { - treeExpandeds.push(row) - } - } - eachTree(tableFullData, rowHandler, treeConfig) - } - this.treeExpandeds = treeExpandeds - - setTreeScrollYCache(this) - - return this.$nextTick().then(this.recalculate) - }, - // 设置展开树形节点,二个参数设置这一行展开与否:支持单行,支持多行 - setTreeExpansion(rows, expanded) { - let { treeConfig, treeExpandeds, tableFullData } = this - let { accordion, children } = treeConfig - let isToggle = arguments.length === 1 - if (!rows) { - return this.$nextTick().then(this.recalculate) - } - if (!isArray(rows)) { - rows = [rows] - } - if (accordion) { - rows = rows.slice(rows.length - 1, rows.length) - } - - // 这里需要进行一次浅拷贝,不能直接操作vue observe的数组,不然vue频繁的get、set、toRaw、reactive等操作从而导致卡顿 - const treeExpandedsCopy = [...treeExpandeds] - rows.forEach((row) => { - if (row[children] && row[children].length) { - const index = treeExpandedsCopy.indexOf(row) - if (accordion) { - // 同一级只能展开一个 - const matchObj = findTree(tableFullData, (item) => item === row, treeConfig) - remove(treeExpandedsCopy, (item) => ~matchObj.items.indexOf(item)) - } - if (~index && (isToggle || !expanded)) { - treeExpandedsCopy.splice(index, 1) - return - } - if (!~index && (isToggle || expanded)) { - treeExpandedsCopy.push(row) - } - } - }) - - this.treeExpandeds = treeExpandedsCopy - - setTreeScrollYCache(this) - - return this.$nextTick().then(this.recalculate) - }, - hasTreeExpand(row) { - return ~this.treeExpandeds.indexOf(row) - }, - clearTreeExpand() { - const hasExpand = this.treeExpandeds.length - this.treeExpandeds = [] - setTreeScrollYCache(this) - return this.$nextTick().then(() => (hasExpand ? this.recalculate() : 0)) - }, // 获取虚拟滚动状态 getVirtualScroller() { let { scrollXLoad, scrollYLoad } = this @@ -2289,105 +1819,6 @@ const Methods = { }, // 检查触发源是否属于目标节点 getEventTargetNode, - initMultipleHistory() { - const { isMultipleHistory, toolBarVm } = this.$grid - const { - settingOpts: { storageKey }, - id: toolbarId - } = toolBarVm - let remoteSelectedMethod, remoteSelectedPromise - - if ( - isMultipleHistory && - toolBarVm && - toolBarVm.setting && - toolBarVm.setting.multipleHistory && - (remoteSelectedMethod = toolBarVm.setting.multipleHistory.remoteSelectedMethod) - ) { - if (typeof remoteSelectedMethod === 'function') { - remoteSelectedPromise = remoteSelectedMethod() - - if (typeof remoteSelectedPromise.then === 'function') { - remoteSelectedPromise.then((storeStr) => { - let storeObj = toStringJSON(storeStr) - storeObj = (storeObj && storeObj[storageKey]) || null - storeObj = (storeObj || {})[toolbarId] || {} - const { columns, pageSize } = storeObj - toolBarVm.applySettings({ columns, pageSize }) - }) - } - } - } - }, - // 显示多选工具栏 - showSelectToolbar() { - let { - $grid: { selectToolbar, showHeader }, - selectToolbarStore - } = this - if (selectToolbar && showHeader) { - selectToolbarStore.visible = false - let selectColumn = find(this.visibleColumn, (item) => item.type === 'selection') - 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') - 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 adjust = 1 - if (selectColumn.fixed === 'right') { - range.setStart(tr, thArr.indexOf(selectTh)) - range.setEnd(tr, thArr.length) - rangeBoundingRect = range.getBoundingClientRect() - layout.left = `${adjust}px` - } else { - range.setStart(tr, 0) - range.setEnd(tr, thArr.indexOf(selectTh) + 1) - rangeBoundingRect = range.getBoundingClientRect() - layout.left = `${rangeBoundingRect.width + adjust}px` - } - layout.width = `${headerBoundingRect.width - rangeBoundingRect.width - 2 * adjust}px` - if (!selectColumn.fixed && position === 'left') { - range = document.createRange() - range.setStart(tr, 0) - range.setEnd(tr, thArr.indexOf(selectTh)) - rangeBoundingRect = range.getBoundingClientRect() - layout.left = `${adjust}px` - layout.width = `${rangeBoundingRect.width - 2 * adjust}px` - } - layout.top = `${headerBoundingRect.height - rangeBoundingRect.height + adjust}px` - layout.height = `${rangeBoundingRect.height - 2 * adjust}px` - return this.$nextTick().then(() => { - selectToolbarStore.layout = layout - selectToolbarStore.visible = true - }) - } - } - return this.$nextTick() - }, - // 切换多选工具栏的显示 - toggleSelectToolbarVisible() { - this.selectToolbarStore.visible = !this.selectToolbarStore.visible - return this.$nextTick() - }, - // 在空数据时Selection列表头复选框禁用,headerAutoDisabled设置为false就会和旧版本兼容 - handleSelectionHeader() { - const { tableFullData, visibleColumn, selectConfig = {} } = this - const { headerAutoDisabled } = selectConfig - const selectionColumn = visibleColumn.find((column) => column.type === 'selection') - if ( - (typeof headerAutoDisabled === 'undefined' || (typeof headerAutoDisabled === 'boolean' && headerAutoDisabled)) && - !tableFullData.length && - selectionColumn - ) { - this.headerCheckDisabled = true - } - }, // 可见性改变事件处理 handleVisibilityChange(visible, entry) { if (visible) { @@ -2416,8 +1847,8 @@ const Methods = { } } funcs.forEach((name) => { - Methods[name] = function () { - return this[`_${name}`] ? this[`_${name}`].apply(this, arguments) : null + Methods[name] = function (...args) { + return this[`_${name}`] ? this[`_${name}`](...args) : null } }) export default Methods diff --git a/packages/vue/src/grid/src/toolbar/index.ts b/packages/vue/src/grid/src/toolbar/index.ts new file mode 100644 index 000000000..3a765dc52 --- /dev/null +++ b/packages/vue/src/grid/src/toolbar/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: 'grid', + install(host) { + Object.assign(host.methods, Methods) + } +} diff --git a/packages/vue/src/grid/src/toolbar/src/methods.ts b/packages/vue/src/grid/src/toolbar/src/methods.ts new file mode 100644 index 000000000..c04fb4f00 --- /dev/null +++ b/packages/vue/src/grid/src/toolbar/src/methods.ts @@ -0,0 +1,280 @@ +import { Buttons } from '../../adapter' +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 { addClass, removeClass } from '@opentiny/vue-renderless/common/deps/dom' +import { extend } from '@opentiny/vue-renderless/common/object' + +export function setBodyRecords({ body, insertRecords, pendingRecords }) { + if (insertRecords.length) { + body.pendingRecords = pendingRecords.filter((row) => !insertRecords.includes(row)) + } + + if (pendingRecords.length) { + body.insertRecords = insertRecords.filter((row) => !pendingRecords.includes(row)) + } +} + +function canInvokeSaveDataApi(body, removeRecords, updateRecords) { + return body.insertRecords.length || removeRecords.length || updateRecords.length || body.pendingRecords.length +} + +export function doRemoveOrShowMsg({ _vm, canInvoke, code, isMsg, pendingRecords, resolve, valid }) { + if (valid && !canInvoke) { + if (isMsg) { + // 直接移除未保存且标记为删除的数据 + if (pendingRecords.length) { + _vm.remove(pendingRecords) + } else { + Modal.message({ + id: code, + message: GlobalConfig.i18n('ui.grid.dataUnchanged'), + status: 'info' + }) + } + } + + resolve() + } +} + +export function invokeSaveDataApi({ _vm, args, body, code, removeRecords, resolve, saveData, updateRecords, valid }) { + let canInvoke = false + + if (valid) { + canInvoke = canInvokeSaveDataApi(body, removeRecords, updateRecords) + } + + if (valid && canInvoke) { + _vm.tableLoading = true + + resolve( + saveData.api + .apply(_vm, [{ $grid: _vm, changeRecords: body }].concat(args)) + .then(() => { + Modal.message({ + id: code, + message: GlobalConfig.i18n('ui.grid.saveSuccess'), + status: 'success' + }) + _vm.tableLoading = false + }) + .catch(() => { + _vm.tableLoading = false + }) + .then(() => _vm.commitProxy('reload')) + ) + } + + return canInvoke +} + +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') + }) + } + + return res + })()) + }, + handleSave(code, args) { + let { saveData, isMsg } = this + + if (!saveData) { + error('ui.grid.error.notSave') + return + } + + let body = extend(true, { pendingRecords: this.pendingRecords }, this.getRecordset()) + let { insertRecords, removeRecords, updateRecords, pendingRecords } = body + let validRows = insertRecords.concat(updateRecords) + let getCallback = (resolve) => (valid) => { + if (!valid) { + resolve(valid) + return + } + + let canInvoke = invokeSaveDataApi({ + _vm: this, + args, + body, + code, + removeRecords, + resolve, + saveData, + updateRecords, + valid + }) + + doRemoveOrShowMsg({ _vm: this, canInvoke, code, isMsg, pendingRecords, resolve, valid }) + } + + // 排除掉新增且标记为删除的数据,排除已标记为删除的数据 + setBodyRecords({ body, insertRecords, pendingRecords }) + + // 只校验新增和修改的数据 + return new Promise((resolve) => { + this.validate(validRows, getCallback(resolve)) + }) + }, + handleDelete(code, args) { + let { deleteData, isMsg } = this + + if (!deleteData) { + error('ui.grid.error.notDelete') + return + } + + let selecteds = this.getSelectRecords(true) + let afterRemove = () => { + let removeds = this.getRemoveRecords() + + if (!removeds.length && isMsg && !selecteds.length) { + Modal.message({ + id: code, + message: GlobalConfig.i18n('ui.grid.selectOneRecord'), + status: 'warning' + }) + } + + if (removeds.length) { + let apiArgs = [{ $grid: this, changeRecords: { removeRecords: removeds } }, ...args] + let stopLoading = () => { + this.tableLoading = false + } + + this.tableLoading = true + + return deleteData.api + .apply(this, apiArgs) + .then(stopLoading) + .catch(stopLoading) + .then(() => this.commitProxy('reload')) + } + } + + this.remove(selecteds).then(afterRemove) + }, + handleFullScreen([show]) { + const cls = 'tiny-fullscreen-full' + + show ? addClass(this.$el, cls) : removeClass(this.$el, cls) + + this.recalculate() + + emitEvent(this, 'fullscreen', show) + this.emitter.emit('fullscreen', show) + }, + commitProxy(code, ...args) { + 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') { + 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') { + this.exportCsv() + } else if (code === 'reset_custom') { + this.resetAll() + } else if (~['reload', 'query', 'prefetch'].indexOf(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) + } + + return this.$nextTick() + }, + handleDeleteRow(code, i18nKey, callback) { + let selecteds = this.getSelectRecords() + + if (this.isMsg && selecteds.length) { + Modal.confirm(GlobalConfig.i18n(i18nKey)).then((type) => { + type === 'confirm' && callback() + }) + } + + if (this.isMsg && !selecteds.length) { + Modal.message({ + id: code, + message: GlobalConfig.i18n('ui.grid.selectOneRecord'), + status: 'warning' + }) + } + + if (!this.isMsg && selecteds.length) { + callback() + } + }, + getPendingRecords() { + return this.pendingRecords + }, + triggerToolbarBtnEvent(button, event) { + let { events = {}, tableListeners } = this + let { code } = button + + if (!events.toolbarButtonClick && !tableListeners['toolbar-button-click']) { + this.commitProxy(code, event) + } + + emitEvent(this, 'toolbar-button-click', [{ code, button, $grid: this }, event]) + + this.emitter.emit('toolbar-button-click', { code, button, $grid: this }, event) + }, + triggerPendingEvent(code) { + let { isMsg, pendingRecords: pendings } = this + let selectColumn = this.getColumns().filter((col) => ~['selection', 'radio'].indexOf(col.type)) + let isSelection = selectColumn.length && selectColumn[0].type === 'selection' + let isRadio = selectColumn.length && selectColumn[0].type === 'radio' + let selecteds = isSelection ? this.getSelectRecords(true) : isRadio ? [this.getRadioRow()] : [] + + if (!selecteds.length && isMsg) { + Modal.message({ + id: code, + message: GlobalConfig.i18n('ui.grid.selectOneRecord'), + status: 'warning' + }) + } + + if (selecteds.length) { + let { plus = [], minus = [], tmp } = {} + + selecteds.forEach((data) => { + let selectedPending = pendings.includes(data) + + tmp = selectedPending ? minus : plus + tmp.push(data) + }) + + tmp = minus.length ? pendings.filter((item) => !~minus.indexOf(item)) : pendings + this.pendingRecords = tmp.concat(plus) + + isSelection && this.clearSelection() + isRadio && this.clearRadioRow() + } + } +} diff --git a/packages/vue/src/grid/src/tooltip/index.ts b/packages/vue/src/grid/src/tooltip/index.ts new file mode 100644 index 000000000..ad8f9d35b --- /dev/null +++ b/packages/vue/src/grid/src/tooltip/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/table/src/utils/handleTooltip.ts b/packages/vue/src/grid/src/tooltip/src/handleTooltip.ts similarity index 100% rename from packages/vue/src/grid/src/table/src/utils/handleTooltip.ts rename to packages/vue/src/grid/src/tooltip/src/handleTooltip.ts diff --git a/packages/vue/src/grid/src/tooltip/src/methods.ts b/packages/vue/src/grid/src/tooltip/src/methods.ts new file mode 100644 index 000000000..f782399a9 --- /dev/null +++ b/packages/vue/src/grid/src/tooltip/src/methods.ts @@ -0,0 +1,133 @@ +import debounce from '@opentiny/vue-renderless/common/deps/debounce' +import { getStyle } from '@opentiny/vue-renderless/common/deps/dom' +import { createTooltipRange, processContentMethod } from './handleTooltip' + +let focusSingle = null + +export default { + // 显示 tooltip 依赖的 popper 组件 + activateTooltip(tooltip, isValid) { + if (!this.tasks.activateTooltip) { + this.tasks.activateTooltip = debounce(300, () => { + let sign = isValid !== undefined ? isValid : focusSingle + + if (sign) { + tooltip.state.popperElm && (tooltip.state.popperElm.style.display = 'none') + tooltip.doDestroy() + tooltip.show() + setTimeout(tooltip.updatePopper) + } + }) + } + + this.tasks.activateTooltip() + }, + // 显示 tooltip 依赖的 popper 组件 + activateTooltipValid(tooltip) { + if (!this.tasks.activateTooltipValid) { + this.tasks.activateTooltipValid = debounce(50, () => { + tooltip.handleShowPopper() + setTimeout(() => tooltip.updatePopper()) + }) + } + + this.tasks.activateTooltipValid() + }, + // 显示 tooltip + handleTooltip(event, column, row, showTip, isHeader) { + const cell = isHeader + ? event.currentTarget.querySelector('.tiny-grid-cell-text') + : event.currentTarget.querySelector('.tiny-grid-cell') + + // 当用户悬浮在排序或者筛选图标按钮时不应该显示tooltip + if (isHeader && event.target !== cell) { + return + } + const tooltip = this.$refs.tooltip + const wrapperElem = cell + const content = cell.innerText.trim() || cell.textContent.trim() + const { contentMethod } = this.tooltipConfig + const range = createTooltipRange({ _vm: this, cell, column, isHeader }) + const rangeWidth = range.getBoundingClientRect().width + const padding = + (parseInt(getStyle(cell, 'paddingLeft'), 10) || 0) + (parseInt(getStyle(cell, 'paddingRight'), 10) || 0) + const isOverflow = rangeWidth + padding > cell.offsetWidth || wrapperElem.scrollWidth > wrapperElem.clientWidth + + // content如果是空字符串,但是用户配置了contentMethod,则同样也可以触发提示 + if ((contentMethod || content) && (showTip || isOverflow)) { + Object.assign(this.tooltipStore, { row, column, visible: true }) + + if (tooltip) { + processContentMethod({ _vm: this, column, content, contentMethod, event, isHeader, row, showTip }) + tooltip.state.referenceElm = cell + tooltip.state.popperElm && (tooltip.state.popperElm.style.display = 'none') + + focusSingle = true + this.activateTooltip(tooltip) + } + } + + return this.$nextTick() + }, + // 提供关闭tips提示的方法 + clostTooltip() { + let tooltip = this.$refs.tooltip + Object.assign(this.tooltipStore, { + content: null, + row: null, + visible: false, + column: null + }) + focusSingle = false + if (tooltip && typeof tooltip.setExpectedState === 'function') { + tooltip.setExpectedState(false) + this.debounceClose(tooltip) + } + return this.$nextTick() + }, + // 添加代码让用户代码可以划入popper + debounceClose(tooltip) { + if (!this.tasks.debounceClose) { + this.tasks.debounceClose = debounce(50, () => { + tooltip.handleClosePopper() + }) + } + + this.tasks.debounceClose() + }, + // 触发表头 tooltip 事件 + triggerHeaderTooltipEvent(event, params) { + let { tooltipStore } = this + let { column, showHeaderTip } = params + if (tooltipStore.column !== column || !tooltipStore.visible) { + // 在 v3.0 中废弃 label + this.handleTooltip(event, column, null, showHeaderTip, true) + } + }, + + // 触发表尾 tooltip 事件 + triggerFooterTooltipEvent(event, params) { + let { column } = params + let tooltipStore = this.tooltipStore + if (tooltipStore.column !== column || !tooltipStore.visible) { + this.handleTooltip(event, column) + } + }, + // 触发 tooltip 事件 + triggerTooltipEvent(event, params) { + let { editConfig, editStore, tooltipStore } = this + let { actived } = editStore + let { row, column, showTip } = params + if (editConfig) { + if ( + (editConfig.mode === 'row' && actived.row === row && column.editor) || + (actived.row === row && actived.column === column) + ) { + return + } + } + if (tooltipStore.column !== column || tooltipStore.row !== row || !tooltipStore.visible) { + this.handleTooltip(event, column, row, showTip) + } + } +} diff --git a/packages/vue/src/grid/src/tree/index.ts b/packages/vue/src/grid/src/tree/index.ts new file mode 100644 index 000000000..ad8f9d35b --- /dev/null +++ b/packages/vue/src/grid/src/tree/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/tree/src/methods.ts b/packages/vue/src/grid/src/tree/src/methods.ts new file mode 100644 index 000000000..6c3b581a5 --- /dev/null +++ b/packages/vue/src/grid/src/tree/src/methods.ts @@ -0,0 +1,126 @@ +import { filterTree, remove, isArray, findTree, get, eachTree } from '@opentiny/vue-renderless/grid/static/' +import { getTableRowKey, setTreeScrollYCache } from '../../table/src/strategy' +import { emitEvent } from '@opentiny/vue-renderless/grid/utils' + +export default { + // 展开树节点事件 + triggerTreeExpandEvent(event, { row }) { + let { currentColumn, currentRow } = this + let rest = this.toggleTreeExpansion(row) + let eventParams = { $table: this, row, rowIndex: this.getRowIndex(row) } + emitEvent(this, 'toggle-tree-change', [eventParams, event]) + this.$nextTick(() => { + if (currentRow) { + this.setCurrentRow(currentRow) + } else if (currentColumn) { + this.setCurrentColumn(currentColumn) + } + }) + return rest + }, + // 切换/展开树节点 + toggleTreeExpansion(row) { + return this.setTreeExpansion(row) + }, + // 处理默认展开树节点 + handleDefaultTreeExpand() { + let { tableFullData, treeConfig } = this + if (!treeConfig) { + return + } + let { children, expandAll, expandRowKeys: rowids } = treeConfig + let treeExpandeds = [] + let rowkey = getTableRowKey(this) + let isNonEmptyArr = (arr) => isArray(arr) && arr.length + // 展开所有行 + let doExpandAll = () => { + filterTree(tableFullData, (row) => isNonEmptyArr(row[children]) && treeExpandeds.push(row), treeConfig) + this.treeExpandeds = treeExpandeds + } + // 展开指定行 + let doExpandRows = () => { + rowids.forEach((rowid) => { + let matchObj = findTree(tableFullData, (item) => rowid === get(item, rowkey), treeConfig) + + matchObj && isNonEmptyArr(matchObj.item[children]) && treeExpandeds.push(matchObj.item) + }) + this.treeExpandeds = treeExpandeds + } + + if (expandAll) { + doExpandAll() + } else if (rowids) { + doExpandRows() + } + + setTreeScrollYCache(this) + }, + setAllTreeExpansion(expanded) { + let { tableFullData, treeConfig } = this + let children = treeConfig.children + let treeExpandeds = [] + if (expanded) { + let rowHandler = (row) => { + if (row[children] && row[children].length) { + treeExpandeds.push(row) + } + } + eachTree(tableFullData, rowHandler, treeConfig) + } + this.treeExpandeds = treeExpandeds + + setTreeScrollYCache(this) + + return this.$nextTick().then(this.recalculate) + }, + // 设置展开树形节点,二个参数设置这一行展开与否:支持单行,支持多行 + setTreeExpansion(rows, expanded) { + let { treeConfig, treeExpandeds, tableFullData } = this + let { accordion, children } = treeConfig + let isToggle = arguments.length === 1 + if (!rows) { + return this.$nextTick().then(this.recalculate) + } + if (!isArray(rows)) { + rows = [rows] + } + if (accordion) { + rows = rows.slice(rows.length - 1, rows.length) + } + + // 这里需要进行一次浅拷贝,不能直接操作vue observe的数组,不然vue频繁的get、set、toRaw、reactive等操作从而导致卡顿 + const treeExpandedsCopy = [...treeExpandeds] + rows.forEach((row) => { + if (row[children] && row[children].length) { + const index = treeExpandedsCopy.indexOf(row) + if (accordion) { + // 同一级只能展开一个 + const matchObj = findTree(tableFullData, (item) => item === row, treeConfig) + remove(treeExpandedsCopy, (item) => ~matchObj.items.indexOf(item)) + } + if (~index && (isToggle || !expanded)) { + treeExpandedsCopy.splice(index, 1) + return + } + if (!~index && (isToggle || expanded)) { + treeExpandedsCopy.push(row) + } + } + }) + + this.treeExpandeds = treeExpandedsCopy + + setTreeScrollYCache(this) + + return this.$nextTick().then(this.recalculate) + }, + hasTreeExpand(row) { + return ~this.treeExpandeds.indexOf(row) + }, + clearTreeExpand() { + const hasExpand = this.treeExpandeds.length + this.treeExpandeds = [] + setTreeScrollYCache(this) + return this.$nextTick().then(() => (hasExpand ? this.recalculate() : 0)) + } +} diff --git a/packages/vue/src/grid/src/types/index.type.ts b/packages/vue/src/grid/src/types/index.type.ts new file mode 100644 index 000000000..56f649e37 --- /dev/null +++ b/packages/vue/src/grid/src/types/index.type.ts @@ -0,0 +1,6 @@ +export interface Plugin { + // 插件所属宿主 + host?: 'grid' | 'table' + // 插件加载方法 + install?: (Table: any) => void +}