mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-02 03:08:21 +08:00
refactor(components): [transfer] switch to script-setup syntax (#8343)
* refactor(components): [transfer] switch to script-setup syntax * chore: improve code * chore: improve type * fix: lost reactivity * chore: fix data error * chore: simplify code
This commit is contained in:
parent
598f3ab5cd
commit
a894fbf990
@ -309,7 +309,7 @@ const useEvent = (
|
||||
}
|
||||
}
|
||||
|
||||
type CheckboxValueType = string | number | boolean
|
||||
export type CheckboxValueType = string | number | boolean
|
||||
|
||||
export const checkboxEmits = {
|
||||
[UPDATE_MODEL_EVENT]: (val: CheckboxValueType) =>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import Transfer from '../src/index.vue'
|
||||
import Transfer from '../src/transfer.vue'
|
||||
|
||||
describe('Transfer', () => {
|
||||
const getTestData = () => {
|
||||
|
@ -1,15 +1,8 @@
|
||||
import Transfer from './src/index.vue'
|
||||
import { withInstall } from '@element-plus/utils'
|
||||
|
||||
import type { App } from 'vue'
|
||||
import type { SFCWithInstall } from '@element-plus/utils'
|
||||
import Transfer from './src/transfer.vue'
|
||||
|
||||
Transfer.install = (app: App): void => {
|
||||
app.component(Transfer.name, Transfer)
|
||||
}
|
||||
|
||||
const _Transfer = Transfer as SFCWithInstall<typeof Transfer>
|
||||
|
||||
export default _Transfer
|
||||
export const ElTransfer = _Transfer
|
||||
export const ElTransfer = withInstall(Transfer)
|
||||
export default ElTransfer
|
||||
|
||||
export * from './src/transfer'
|
||||
|
5
packages/components/transfer/src/composables/index.ts
Normal file
5
packages/components/transfer/src/composables/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './use-check'
|
||||
export * from './use-checked-change'
|
||||
export * from './use-computed-data'
|
||||
export * from './use-move'
|
||||
export * from './use-props-alias'
|
@ -1,54 +1,40 @@
|
||||
// @ts-nocheck
|
||||
import { computed, getCurrentInstance, watch } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
import { isFunction } from '@element-plus/utils'
|
||||
import { CHECKED_CHANGE_EVENT } from '../transfer-panel'
|
||||
import { usePropsAlias } from './use-props-alias'
|
||||
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type { Key, TransferPanelState } from './transfer'
|
||||
|
||||
export const CHECKED_CHANGE_EVENT = 'checked-change'
|
||||
|
||||
export const useCheckProps = {
|
||||
data: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
optionRender: Function,
|
||||
placeholder: String,
|
||||
title: String,
|
||||
filterable: Boolean,
|
||||
format: Object,
|
||||
filterMethod: Function,
|
||||
defaultChecked: Array,
|
||||
props: Object,
|
||||
}
|
||||
import type { SetupContext } from 'vue'
|
||||
import type { CheckboxValueType } from '@element-plus/components/checkbox'
|
||||
import type { TransferKey } from '../transfer'
|
||||
import type {
|
||||
TransferPanelEmits,
|
||||
TransferPanelProps,
|
||||
TransferPanelState,
|
||||
} from '../transfer-panel'
|
||||
|
||||
export const useCheck = (
|
||||
props: ExtractPropTypes<typeof useCheckProps>,
|
||||
panelState: TransferPanelState
|
||||
props: TransferPanelProps,
|
||||
panelState: TransferPanelState,
|
||||
emit: SetupContext<TransferPanelEmits>['emit']
|
||||
) => {
|
||||
const { emit } = getCurrentInstance()
|
||||
|
||||
const labelProp = computed(() => props.props.label || 'label')
|
||||
|
||||
const keyProp = computed(() => props.props.key || 'key')
|
||||
|
||||
const disabledProp = computed(() => props.props.disabled || 'disabled')
|
||||
const propsAlias = usePropsAlias(props)
|
||||
|
||||
const filteredData = computed(() => {
|
||||
return props.data.filter((item) => {
|
||||
if (typeof props.filterMethod === 'function') {
|
||||
if (isFunction(props.filterMethod)) {
|
||||
return props.filterMethod(panelState.query, item)
|
||||
} else {
|
||||
const label = item[labelProp.value] || item[keyProp.value].toString()
|
||||
const label = String(
|
||||
item[propsAlias.value.label] || item[propsAlias.value.key]
|
||||
)
|
||||
return label.toLowerCase().includes(panelState.query.toLowerCase())
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const checkableData = computed(() => {
|
||||
return filteredData.value.filter((item) => !item[disabledProp.value])
|
||||
})
|
||||
const checkableData = computed(() =>
|
||||
filteredData.value.filter((item) => !item[propsAlias.value.disabled])
|
||||
)
|
||||
|
||||
const checkedSummary = computed(() => {
|
||||
const checkedLength = panelState.checked.length
|
||||
@ -73,16 +59,16 @@ export const useCheck = (
|
||||
|
||||
const updateAllChecked = () => {
|
||||
const checkableDataKeys = checkableData.value.map(
|
||||
(item) => item[keyProp.value]
|
||||
(item) => item[propsAlias.value.key]
|
||||
)
|
||||
panelState.allChecked =
|
||||
checkableDataKeys.length > 0 &&
|
||||
checkableDataKeys.every((item) => panelState.checked.includes(item))
|
||||
}
|
||||
|
||||
const handleAllCheckedChange = (value: Key[]) => {
|
||||
const handleAllCheckedChange = (value: CheckboxValueType) => {
|
||||
panelState.checked = value
|
||||
? checkableData.value.map((item) => item[keyProp.value])
|
||||
? checkableData.value.map((item) => item[propsAlias.value.key])
|
||||
: []
|
||||
}
|
||||
|
||||
@ -110,9 +96,9 @@ export const useCheck = (
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
const checked = []
|
||||
const checked: TransferKey[] = []
|
||||
const filteredDataKeys = filteredData.value.map(
|
||||
(item) => item[keyProp.value]
|
||||
(item) => item[propsAlias.value.key]
|
||||
)
|
||||
panelState.checked.forEach((item) => {
|
||||
if (filteredDataKeys.includes(item)) {
|
||||
@ -134,9 +120,9 @@ export const useCheck = (
|
||||
)
|
||||
return
|
||||
|
||||
const checked = []
|
||||
const checked: TransferKey[] = []
|
||||
const checkableDataKeys = checkableData.value.map(
|
||||
(item) => item[keyProp.value]
|
||||
(item) => item[propsAlias.value.key]
|
||||
)
|
||||
|
||||
val.forEach((item) => {
|
||||
@ -153,9 +139,6 @@ export const useCheck = (
|
||||
)
|
||||
|
||||
return {
|
||||
labelProp,
|
||||
keyProp,
|
||||
disabledProp,
|
||||
filteredData,
|
||||
checkableData,
|
||||
checkedSummary,
|
@ -0,0 +1,36 @@
|
||||
import { LEFT_CHECK_CHANGE_EVENT, RIGHT_CHECK_CHANGE_EVENT } from '../transfer'
|
||||
|
||||
import type { SetupContext } from 'vue'
|
||||
import type {
|
||||
TransferCheckedState,
|
||||
TransferEmits,
|
||||
TransferKey,
|
||||
} from '../transfer'
|
||||
|
||||
export const useCheckedChange = (
|
||||
checkedState: TransferCheckedState,
|
||||
emit: SetupContext<TransferEmits>['emit']
|
||||
) => {
|
||||
const onSourceCheckedChange = (
|
||||
val: TransferKey[],
|
||||
movedKeys?: TransferKey[]
|
||||
) => {
|
||||
checkedState.leftChecked = val
|
||||
if (!movedKeys) return
|
||||
emit(LEFT_CHECK_CHANGE_EVENT, val, movedKeys)
|
||||
}
|
||||
|
||||
const onTargetCheckedChange = (
|
||||
val: TransferKey[],
|
||||
movedKeys?: TransferKey[]
|
||||
) => {
|
||||
checkedState.rightChecked = val
|
||||
if (!movedKeys) return
|
||||
emit(RIGHT_CHECK_CHANGE_EVENT, val, movedKeys)
|
||||
}
|
||||
|
||||
return {
|
||||
onSourceCheckedChange,
|
||||
onTargetCheckedChange,
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { computed } from 'vue'
|
||||
import { usePropsAlias } from './use-props-alias'
|
||||
|
||||
import type { TransferDataItem, TransferKey, TransferProps } from '../transfer'
|
||||
|
||||
export const useComputedData = (props: TransferProps) => {
|
||||
const propsAlias = usePropsAlias(props)
|
||||
|
||||
const dataObj = computed(() =>
|
||||
props.data.reduce((o, cur) => (o[cur[propsAlias.value.key]] = cur) && o, {})
|
||||
)
|
||||
|
||||
const sourceData = computed(() =>
|
||||
props.data.filter(
|
||||
(item) => !props.modelValue.includes(item[propsAlias.value.key])
|
||||
)
|
||||
)
|
||||
|
||||
const targetData = computed(() => {
|
||||
if (props.targetOrder === 'original') {
|
||||
return props.data.filter((item) =>
|
||||
props.modelValue.includes(item[propsAlias.value.key])
|
||||
)
|
||||
} else {
|
||||
return props.modelValue.reduce(
|
||||
(arr: TransferDataItem[], cur: TransferKey) => {
|
||||
const val = dataObj.value[cur]
|
||||
if (val) {
|
||||
arr.push(val)
|
||||
}
|
||||
return arr
|
||||
},
|
||||
[]
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
sourceData,
|
||||
targetData,
|
||||
}
|
||||
}
|
@ -1,23 +1,30 @@
|
||||
// @ts-nocheck
|
||||
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { usePropsAlias } from './use-props-alias'
|
||||
|
||||
import type { ComputedRef } from 'vue'
|
||||
import type { SetupContext } from 'vue'
|
||||
import type {
|
||||
DataItem,
|
||||
Key,
|
||||
TransferCheckedState,
|
||||
TransferDataItem,
|
||||
TransferDirection,
|
||||
TransferEmits,
|
||||
TransferKey,
|
||||
TransferProps,
|
||||
} from './transfer'
|
||||
} from '../transfer'
|
||||
|
||||
export const useMove = (
|
||||
props: TransferProps,
|
||||
checkedState: TransferCheckedState,
|
||||
propsKey: ComputedRef<string>,
|
||||
emit
|
||||
emit: SetupContext<TransferEmits>['emit']
|
||||
) => {
|
||||
const _emit = (value, type: 'left' | 'right', checked: Key[]) => {
|
||||
const propsAlias = usePropsAlias(props)
|
||||
|
||||
const _emit = (
|
||||
value: TransferKey[],
|
||||
direction: TransferDirection,
|
||||
movedKeys: TransferKey[]
|
||||
) => {
|
||||
emit(UPDATE_MODEL_EVENT, value)
|
||||
emit(CHANGE_EVENT, value, type, checked)
|
||||
emit(CHANGE_EVENT, value, direction, movedKeys)
|
||||
}
|
||||
|
||||
const addToLeft = () => {
|
||||
@ -36,14 +43,14 @@ export const useMove = (
|
||||
let currentValue = props.modelValue.slice()
|
||||
|
||||
const itemsToBeMoved = props.data
|
||||
.filter((item: DataItem) => {
|
||||
const itemKey = item[propsKey.value]
|
||||
.filter((item: TransferDataItem) => {
|
||||
const itemKey = item[propsAlias.value.key]
|
||||
return (
|
||||
checkedState.leftChecked.includes(itemKey) &&
|
||||
!props.modelValue.includes(itemKey)
|
||||
)
|
||||
})
|
||||
.map((item) => item[propsKey.value])
|
||||
.map((item) => item[propsAlias.value.key])
|
||||
|
||||
currentValue =
|
||||
props.targetOrder === 'unshift'
|
||||
@ -52,8 +59,8 @@ export const useMove = (
|
||||
|
||||
if (props.targetOrder === 'original') {
|
||||
currentValue = props.data
|
||||
.filter((item) => currentValue.includes(item[propsKey.value]))
|
||||
.map((item) => item[propsKey.value])
|
||||
.filter((item) => currentValue.includes(item[propsAlias.value.key]))
|
||||
.map((item) => item[propsAlias.value.key])
|
||||
}
|
||||
|
||||
_emit(currentValue, 'right', checkedState.leftChecked)
|
@ -0,0 +1,16 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { TransferPropsAlias } from '../transfer'
|
||||
|
||||
export const usePropsAlias = (props: { props: TransferPropsAlias }) => {
|
||||
const initProps: Required<TransferPropsAlias> = {
|
||||
label: 'label',
|
||||
key: 'key',
|
||||
disabled: 'disabled',
|
||||
}
|
||||
|
||||
return computed(() => ({
|
||||
...initProps,
|
||||
...props.props,
|
||||
}))
|
||||
}
|
@ -1,257 +0,0 @@
|
||||
<template>
|
||||
<div :class="ns.b()">
|
||||
<transfer-panel
|
||||
ref="leftPanel"
|
||||
:data="sourceData"
|
||||
:option-render="optionRender"
|
||||
:placeholder="panelFilterPlaceholder"
|
||||
:title="leftPanelTitle"
|
||||
:filterable="filterable"
|
||||
:format="format"
|
||||
:filter-method="filterMethod"
|
||||
:default-checked="leftDefaultChecked"
|
||||
:props="props"
|
||||
@checked-change="onSourceCheckedChange"
|
||||
>
|
||||
<slot name="left-footer" />
|
||||
</transfer-panel>
|
||||
<div :class="ns.e('buttons')">
|
||||
<el-button
|
||||
type="primary"
|
||||
:class="[ns.e('button'), ns.is('with-texts', hasButtonTexts)]"
|
||||
:disabled="rightChecked.length === 0"
|
||||
@click="addToLeft"
|
||||
>
|
||||
<el-icon><arrow-left /></el-icon>
|
||||
<span v-if="buttonTexts[0] !== undefined">{{ buttonTexts[0] }}</span>
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:class="[ns.e('button'), ns.is('with-texts', hasButtonTexts)]"
|
||||
:disabled="leftChecked.length === 0"
|
||||
@click="addToRight"
|
||||
>
|
||||
<span v-if="buttonTexts[1] !== undefined">{{ buttonTexts[1] }}</span>
|
||||
<el-icon><arrow-right /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<transfer-panel
|
||||
ref="rightPanel"
|
||||
:data="targetData"
|
||||
:option-render="optionRender"
|
||||
:placeholder="panelFilterPlaceholder"
|
||||
:filterable="filterable"
|
||||
:format="format"
|
||||
:filter-method="filterMethod"
|
||||
:title="rightPanelTitle"
|
||||
:default-checked="rightDefaultChecked"
|
||||
:props="props"
|
||||
@checked-change="onTargetCheckedChange"
|
||||
>
|
||||
<slot name="right-footer" />
|
||||
</transfer-panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
h,
|
||||
inject,
|
||||
reactive,
|
||||
ref,
|
||||
toRefs,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import ElButton from '@element-plus/components/button'
|
||||
import ElIcon from '@element-plus/components/icon'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { useLocale, useNamespace } from '@element-plus/hooks'
|
||||
import { formItemContextKey } from '@element-plus/tokens'
|
||||
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
|
||||
import { debugWarn } from '@element-plus/utils'
|
||||
import TransferPanel from './transfer-panel.vue'
|
||||
import { useComputedData } from './useComputedData'
|
||||
import {
|
||||
LEFT_CHECK_CHANGE_EVENT,
|
||||
RIGHT_CHECK_CHANGE_EVENT,
|
||||
useCheckedChange,
|
||||
} from './useCheckedChange'
|
||||
import { useMove } from './useMove'
|
||||
import { CHANGE_EVENT } from './transfer'
|
||||
|
||||
import type { PropType, VNode } from 'vue'
|
||||
import type { FormItemContext } from '@element-plus/tokens'
|
||||
import type { DataItem, Format, Key, Props, TargetOrder } from './transfer'
|
||||
|
||||
type TransferType = InstanceType<typeof TransferPanel>
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElTransfer',
|
||||
|
||||
components: {
|
||||
TransferPanel,
|
||||
ElButton,
|
||||
ElIcon,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
},
|
||||
|
||||
props: {
|
||||
data: {
|
||||
type: Array as PropType<DataItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
titles: {
|
||||
type: Array as PropType<any> as PropType<[string, string]>,
|
||||
default: () => [],
|
||||
},
|
||||
buttonTexts: {
|
||||
type: Array as PropType<any> as PropType<[string, string]>,
|
||||
default: () => [],
|
||||
},
|
||||
filterPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
filterMethod: Function as PropType<
|
||||
(query: string, item: DataItem) => boolean
|
||||
>,
|
||||
leftDefaultChecked: {
|
||||
type: Array as PropType<Key[]>,
|
||||
default: () => [],
|
||||
},
|
||||
rightDefaultChecked: {
|
||||
type: Array as PropType<Key[]>,
|
||||
default: () => [],
|
||||
},
|
||||
renderContent: Function as PropType<(h, option) => VNode>,
|
||||
modelValue: {
|
||||
type: Array as PropType<Key[]>,
|
||||
default: () => [],
|
||||
},
|
||||
format: {
|
||||
type: Object as PropType<Format>,
|
||||
default: () => ({}),
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
props: {
|
||||
type: Object as PropType<Props>,
|
||||
default: () => ({
|
||||
label: 'label',
|
||||
key: 'key',
|
||||
disabled: 'disabled',
|
||||
}),
|
||||
},
|
||||
targetOrder: {
|
||||
type: String as PropType<TargetOrder>,
|
||||
default: 'original',
|
||||
validator: (val: string) => {
|
||||
return ['original', 'push', 'unshift'].includes(val)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
emits: [
|
||||
UPDATE_MODEL_EVENT,
|
||||
CHANGE_EVENT,
|
||||
LEFT_CHECK_CHANGE_EVENT,
|
||||
RIGHT_CHECK_CHANGE_EVENT,
|
||||
],
|
||||
|
||||
setup(props, { emit, slots }) {
|
||||
const { t } = useLocale()
|
||||
const ns = useNamespace('transfer')
|
||||
const elFormItem = inject(formItemContextKey, {} as FormItemContext)
|
||||
|
||||
const checkedState = reactive({
|
||||
leftChecked: [],
|
||||
rightChecked: [],
|
||||
})
|
||||
|
||||
const { propsKey, sourceData, targetData } = useComputedData(props)
|
||||
|
||||
const { onSourceCheckedChange, onTargetCheckedChange } = useCheckedChange(
|
||||
checkedState,
|
||||
emit
|
||||
)
|
||||
|
||||
const { addToLeft, addToRight } = useMove(
|
||||
props,
|
||||
checkedState,
|
||||
propsKey,
|
||||
emit
|
||||
)
|
||||
|
||||
const leftPanel = ref<TransferType>()
|
||||
const rightPanel = ref<TransferType>()
|
||||
|
||||
const clearQuery = (which: 'left' | 'right') => {
|
||||
switch (which) {
|
||||
case 'left':
|
||||
leftPanel.value!.query = ''
|
||||
break
|
||||
case 'right':
|
||||
rightPanel.value!.query = ''
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const hasButtonTexts = computed(() => props.buttonTexts.length === 2)
|
||||
|
||||
const leftPanelTitle = computed(
|
||||
() => props.titles[0] || t('el.transfer.titles.0')
|
||||
)
|
||||
|
||||
const rightPanelTitle = computed(
|
||||
() => props.titles[1] || t('el.transfer.titles.1')
|
||||
)
|
||||
|
||||
const panelFilterPlaceholder = computed(
|
||||
() => props.filterPlaceholder || t('el.transfer.filterPlaceholder')
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
elFormItem.validate?.('change').catch((err) => debugWarn(err))
|
||||
}
|
||||
)
|
||||
|
||||
const optionRender = computed(() => (option) => {
|
||||
if (props.renderContent) return props.renderContent(h, option)
|
||||
|
||||
if (slots.default) return slots.default({ option })
|
||||
|
||||
return h('span', option[props.props.label] || option[props.props.key])
|
||||
})
|
||||
|
||||
return {
|
||||
ns,
|
||||
sourceData,
|
||||
targetData,
|
||||
onSourceCheckedChange,
|
||||
onTargetCheckedChange,
|
||||
addToLeft,
|
||||
addToRight,
|
||||
|
||||
...toRefs(checkedState),
|
||||
|
||||
hasButtonTexts,
|
||||
leftPanelTitle,
|
||||
rightPanelTitle,
|
||||
panelFilterPlaceholder,
|
||||
clearQuery,
|
||||
leftPanel,
|
||||
rightPanel,
|
||||
|
||||
optionRender,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
40
packages/components/transfer/src/transfer-panel.ts
Normal file
40
packages/components/transfer/src/transfer-panel.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { buildProps, definePropType } from '@element-plus/utils'
|
||||
import { transferCheckedChangeFn, transferProps } from './transfer'
|
||||
|
||||
import type { ExtractPropTypes, VNode } from 'vue'
|
||||
import type { TransferDataItem, TransferKey } from './transfer'
|
||||
import type TransferPanel from './transfer-panel.vue'
|
||||
|
||||
export interface TransferPanelState {
|
||||
checked: TransferKey[]
|
||||
allChecked: boolean
|
||||
query: string
|
||||
inputHover: boolean
|
||||
checkChangeByUser: boolean
|
||||
}
|
||||
|
||||
export const CHECKED_CHANGE_EVENT = 'checked-change'
|
||||
|
||||
export const transferPanelProps = buildProps({
|
||||
data: transferProps.data,
|
||||
optionRender: {
|
||||
type: definePropType<(option: TransferDataItem) => VNode | VNode[]>(
|
||||
Function
|
||||
),
|
||||
},
|
||||
placeholder: String,
|
||||
title: String,
|
||||
filterable: Boolean,
|
||||
format: transferProps.format,
|
||||
filterMethod: transferProps.filterMethod,
|
||||
defaultChecked: transferProps.leftDefaultChecked,
|
||||
props: transferProps.props,
|
||||
} as const)
|
||||
export type TransferPanelProps = ExtractPropTypes<typeof transferPanelProps>
|
||||
|
||||
export const transferPanelEmits = {
|
||||
[CHECKED_CHANGE_EVENT]: transferCheckedChangeFn,
|
||||
}
|
||||
export type TransferPanelEmits = typeof transferPanelEmits
|
||||
|
||||
export type TransferPanelInstance = InstanceType<typeof TransferPanel>
|
@ -18,30 +18,27 @@
|
||||
:class="ns.be('panel', 'filter')"
|
||||
size="default"
|
||||
:placeholder="placeholder"
|
||||
:prefix-icon="SearchIcon"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
@mouseenter="inputHover = true"
|
||||
@mouseleave="inputHover = false"
|
||||
/>
|
||||
<el-checkbox-group
|
||||
v-show="!hasNoMatch && data.length > 0"
|
||||
v-show="!hasNoMatch && !isEmpty(data)"
|
||||
v-model="checked"
|
||||
:class="[ns.is('filterable', filterable), ns.be('panel', 'list')]"
|
||||
>
|
||||
<el-checkbox
|
||||
v-for="item in filteredData"
|
||||
:key="item[keyProp]"
|
||||
:key="item[propsAlias.key]"
|
||||
:class="ns.be('panel', 'item')"
|
||||
:label="item[keyProp]"
|
||||
:disabled="item[disabledProp]"
|
||||
:label="item[propsAlias.key]"
|
||||
:disabled="item[propsAlias.disabled]"
|
||||
>
|
||||
<option-content :option="optionRender(item)" />
|
||||
<option-content :option="optionRender?.(item)" />
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<p
|
||||
v-show="hasNoMatch || data.length === 0"
|
||||
:class="ns.be('panel', 'empty')"
|
||||
>
|
||||
<p v-show="hasNoMatch || isEmpty(data)" :class="ns.be('panel', 'empty')">
|
||||
{{ hasNoMatch ? t('el.transfer.noMatch') : t('el.transfer.noData') }}
|
||||
</p>
|
||||
</div>
|
||||
@ -51,82 +48,59 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import { computed, defineComponent, reactive, toRefs } from 'vue'
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, toRefs, useSlots } from 'vue'
|
||||
import { isEmpty } from '@element-plus/utils'
|
||||
import { useLocale, useNamespace } from '@element-plus/hooks'
|
||||
import { ElCheckbox, ElCheckboxGroup } from '@element-plus/components/checkbox'
|
||||
import ElInput from '@element-plus/components/input'
|
||||
import { ElInput } from '@element-plus/components/input'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { CHECKED_CHANGE_EVENT, useCheck, useCheckProps } from './useCheck'
|
||||
import { transferPanelEmits, transferPanelProps } from './transfer-panel'
|
||||
import { useCheck, usePropsAlias } from './composables'
|
||||
|
||||
export default defineComponent({
|
||||
import type { VNode } from 'vue'
|
||||
import type { TransferPanelState } from './transfer-panel'
|
||||
|
||||
defineOptions({
|
||||
name: 'ElTransferPanel',
|
||||
})
|
||||
|
||||
components: {
|
||||
ElCheckboxGroup,
|
||||
ElCheckbox,
|
||||
ElInput,
|
||||
OptionContent: ({ option }) => option,
|
||||
},
|
||||
const props = defineProps(transferPanelProps)
|
||||
const emit = defineEmits(transferPanelEmits)
|
||||
const slots = useSlots()
|
||||
|
||||
props: useCheckProps,
|
||||
const OptionContent = ({ option }: { option: VNode | VNode[] }) => option
|
||||
|
||||
emits: [CHECKED_CHANGE_EVENT],
|
||||
const { t } = useLocale()
|
||||
const ns = useNamespace('transfer')
|
||||
|
||||
setup(props, { slots }) {
|
||||
const { t } = useLocale()
|
||||
const ns = useNamespace('transfer')
|
||||
const panelState = reactive<TransferPanelState>({
|
||||
checked: [],
|
||||
allChecked: false,
|
||||
query: '',
|
||||
inputHover: false,
|
||||
checkChangeByUser: true,
|
||||
})
|
||||
|
||||
const panelState = reactive({
|
||||
checked: [],
|
||||
allChecked: false,
|
||||
query: '',
|
||||
inputHover: false,
|
||||
checkChangeByUser: true,
|
||||
})
|
||||
const propsAlias = usePropsAlias(props)
|
||||
|
||||
const {
|
||||
labelProp,
|
||||
keyProp,
|
||||
disabledProp,
|
||||
filteredData,
|
||||
checkedSummary,
|
||||
isIndeterminate,
|
||||
handleAllCheckedChange,
|
||||
} = useCheck(props, panelState)
|
||||
const {
|
||||
filteredData,
|
||||
checkedSummary,
|
||||
isIndeterminate,
|
||||
handleAllCheckedChange,
|
||||
} = useCheck(props, panelState, emit)
|
||||
|
||||
const hasNoMatch = computed(() => {
|
||||
return panelState.query.length > 0 && filteredData.value.length === 0
|
||||
})
|
||||
const hasNoMatch = computed(
|
||||
() => !isEmpty(panelState.query) && isEmpty(filteredData.value)
|
||||
)
|
||||
|
||||
const hasFooter = computed(() => !!slots.default()[0].children.length)
|
||||
const hasFooter = computed(() => !isEmpty(slots.default!()[0].children))
|
||||
|
||||
const { checked, allChecked, query, inputHover, checkChangeByUser } =
|
||||
toRefs(panelState)
|
||||
const { checked, allChecked, query, inputHover } = toRefs(panelState)
|
||||
|
||||
return {
|
||||
ns,
|
||||
labelProp,
|
||||
keyProp,
|
||||
disabledProp,
|
||||
filteredData,
|
||||
checkedSummary,
|
||||
isIndeterminate,
|
||||
handleAllCheckedChange,
|
||||
|
||||
checked,
|
||||
allChecked,
|
||||
query,
|
||||
inputHover,
|
||||
checkChangeByUser,
|
||||
|
||||
hasNoMatch,
|
||||
SearchIcon: Search,
|
||||
hasFooter,
|
||||
|
||||
t,
|
||||
}
|
||||
},
|
||||
defineExpose({
|
||||
/** @description filter keyword */
|
||||
query,
|
||||
})
|
||||
</script>
|
||||
|
@ -1,68 +1,113 @@
|
||||
// @ts-nocheck
|
||||
import { CHANGE_EVENT } from '@element-plus/constants'
|
||||
import { isNil } from 'lodash-unified'
|
||||
import {
|
||||
buildProps,
|
||||
definePropType,
|
||||
isArray,
|
||||
mutable,
|
||||
} from '@element-plus/utils'
|
||||
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
|
||||
import type { VNode } from 'vue'
|
||||
import type { ExtractPropTypes, h as H, VNode } from 'vue'
|
||||
import type Transfer from './transfer.vue'
|
||||
|
||||
export { CHANGE_EVENT }
|
||||
export type TransferKey = string | number
|
||||
export type TransferDirection = 'left' | 'right'
|
||||
|
||||
export type Key = string | number
|
||||
export type TransferDataItem = Record<string, any>
|
||||
|
||||
export type DataItem = {
|
||||
key: Key
|
||||
label: string
|
||||
disabled: boolean
|
||||
export interface TransferFormat {
|
||||
noChecked?: string
|
||||
hasChecked?: string
|
||||
}
|
||||
|
||||
export type Format = {
|
||||
noChecked: string
|
||||
hasChecked: string
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
label: string
|
||||
key: string
|
||||
disabled: string
|
||||
}
|
||||
|
||||
export type TargetOrder = 'original' | 'push' | 'unshift'
|
||||
|
||||
export interface TransferProps {
|
||||
data: DataItem[]
|
||||
titles: [string, string]
|
||||
buttonTexts: [string, string]
|
||||
filterPlaceholder: string
|
||||
filterMethod?: (query: string, item: DataItem) => boolean
|
||||
leftDefaultChecked: Key[]
|
||||
rightDefaultChecked: Key[]
|
||||
renderContent?: (h, option) => VNode
|
||||
modelValue: Key[]
|
||||
format: Format
|
||||
filterable: boolean
|
||||
props: Props
|
||||
targetOrder: TargetOrder
|
||||
export interface TransferPropsAlias {
|
||||
label?: string
|
||||
key?: string
|
||||
disabled?: string
|
||||
}
|
||||
|
||||
export interface TransferCheckedState {
|
||||
leftChecked: Key[]
|
||||
rightChecked: Key[]
|
||||
leftChecked: TransferKey[]
|
||||
rightChecked: TransferKey[]
|
||||
}
|
||||
|
||||
export interface TransferPanelProps {
|
||||
data: DataItem[]
|
||||
optionRender: ({ option: VNode }) => VNode
|
||||
placeholder: string
|
||||
title: string
|
||||
filterable: boolean
|
||||
format: Format
|
||||
filterMethod: (query: string, item: DataItem) => boolean
|
||||
defaultChecked: Key[]
|
||||
props: Props
|
||||
}
|
||||
export const LEFT_CHECK_CHANGE_EVENT = 'left-check-change'
|
||||
export const RIGHT_CHECK_CHANGE_EVENT = 'right-check-change'
|
||||
|
||||
export interface TransferPanelState {
|
||||
checked: Key[]
|
||||
allChecked: boolean
|
||||
query: string
|
||||
inputHover: boolean
|
||||
checkChangeByUser: boolean
|
||||
export const transferProps = buildProps({
|
||||
data: {
|
||||
type: definePropType<TransferDataItem[]>(Array),
|
||||
default: () => [],
|
||||
},
|
||||
titles: {
|
||||
type: definePropType<[string, string]>(Array),
|
||||
default: () => [],
|
||||
},
|
||||
buttonTexts: {
|
||||
type: definePropType<[string, string]>(Array),
|
||||
default: () => [],
|
||||
},
|
||||
filterPlaceholder: String,
|
||||
filterMethod: {
|
||||
type: definePropType<(query: string, item: TransferDataItem) => boolean>(
|
||||
Function
|
||||
),
|
||||
},
|
||||
leftDefaultChecked: {
|
||||
type: definePropType<TransferKey[]>(Array),
|
||||
default: () => [],
|
||||
},
|
||||
rightDefaultChecked: {
|
||||
type: definePropType<TransferKey[]>(Array),
|
||||
default: () => [],
|
||||
},
|
||||
renderContent: {
|
||||
type: definePropType<
|
||||
(h: typeof H, option: TransferDataItem) => VNode | VNode[]
|
||||
>(Function),
|
||||
},
|
||||
modelValue: {
|
||||
type: definePropType<TransferKey[]>(Array),
|
||||
default: () => [],
|
||||
},
|
||||
format: {
|
||||
type: definePropType<TransferFormat>(Object),
|
||||
default: () => ({}),
|
||||
},
|
||||
filterable: Boolean,
|
||||
props: {
|
||||
type: definePropType<TransferPropsAlias>(Object),
|
||||
default: () =>
|
||||
mutable({
|
||||
label: 'label',
|
||||
key: 'key',
|
||||
disabled: 'disabled',
|
||||
} as const),
|
||||
},
|
||||
targetOrder: {
|
||||
type: String,
|
||||
values: ['original', 'push', 'unshift'],
|
||||
default: 'original',
|
||||
},
|
||||
} as const)
|
||||
export type TransferProps = ExtractPropTypes<typeof transferProps>
|
||||
|
||||
export const transferCheckedChangeFn = (
|
||||
value: TransferKey[],
|
||||
movedKeys?: TransferKey[]
|
||||
) => [value, movedKeys].every(isArray) || (isArray(value) && isNil(movedKeys))
|
||||
|
||||
export const transferEmits = {
|
||||
[CHANGE_EVENT]: (
|
||||
value: TransferKey[],
|
||||
direction: TransferDirection,
|
||||
movedKeys: TransferKey[]
|
||||
) =>
|
||||
[value, movedKeys].every(isArray) && ['left', 'right'].includes(direction),
|
||||
[UPDATE_MODEL_EVENT]: (value: TransferKey[]) => isArray(value),
|
||||
[LEFT_CHECK_CHANGE_EVENT]: transferCheckedChangeFn,
|
||||
[RIGHT_CHECK_CHANGE_EVENT]: transferCheckedChangeFn,
|
||||
}
|
||||
export type TransferEmits = typeof transferEmits
|
||||
|
||||
export type TransferInstance = InstanceType<typeof Transfer>
|
||||
|
161
packages/components/transfer/src/transfer.vue
Normal file
161
packages/components/transfer/src/transfer.vue
Normal file
@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div :class="ns.b()">
|
||||
<transfer-panel
|
||||
ref="leftPanel"
|
||||
:data="sourceData"
|
||||
:option-render="optionRender"
|
||||
:placeholder="panelFilterPlaceholder"
|
||||
:title="leftPanelTitle"
|
||||
:filterable="filterable"
|
||||
:format="format"
|
||||
:filter-method="filterMethod"
|
||||
:default-checked="leftDefaultChecked"
|
||||
:props="props.props"
|
||||
@checked-change="onSourceCheckedChange"
|
||||
>
|
||||
<slot name="left-footer" />
|
||||
</transfer-panel>
|
||||
<div :class="ns.e('buttons')">
|
||||
<el-button
|
||||
type="primary"
|
||||
:class="[ns.e('button'), ns.is('with-texts', hasButtonTexts)]"
|
||||
:disabled="isEmpty(checkedState.rightChecked)"
|
||||
@click="addToLeft"
|
||||
>
|
||||
<el-icon><arrow-left /></el-icon>
|
||||
<span v-if="!isUndefined(buttonTexts[0])">{{ buttonTexts[0] }}</span>
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:class="[ns.e('button'), ns.is('with-texts', hasButtonTexts)]"
|
||||
:disabled="isEmpty(checkedState.leftChecked)"
|
||||
@click="addToRight"
|
||||
>
|
||||
<span v-if="!isUndefined(buttonTexts[1])">{{ buttonTexts[1] }}</span>
|
||||
<el-icon><arrow-right /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<transfer-panel
|
||||
ref="rightPanel"
|
||||
:data="targetData"
|
||||
:option-render="optionRender"
|
||||
:placeholder="panelFilterPlaceholder"
|
||||
:filterable="filterable"
|
||||
:format="format"
|
||||
:filter-method="filterMethod"
|
||||
:title="rightPanelTitle"
|
||||
:default-checked="rightDefaultChecked"
|
||||
:props="props.props"
|
||||
@checked-change="onTargetCheckedChange"
|
||||
>
|
||||
<slot name="right-footer" />
|
||||
</transfer-panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, reactive, ref, useSlots, watch } from 'vue'
|
||||
import { debugWarn, isEmpty, isUndefined } from '@element-plus/utils'
|
||||
import { useFormItem, useLocale, useNamespace } from '@element-plus/hooks'
|
||||
import { ElButton } from '@element-plus/components/button'
|
||||
import { ElIcon } from '@element-plus/components/icon'
|
||||
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
|
||||
import { transferEmits, transferProps } from './transfer'
|
||||
import {
|
||||
useCheckedChange,
|
||||
useComputedData,
|
||||
useMove,
|
||||
usePropsAlias,
|
||||
} from './composables'
|
||||
import TransferPanel from './transfer-panel.vue'
|
||||
|
||||
import type {
|
||||
TransferCheckedState,
|
||||
TransferDataItem,
|
||||
TransferDirection,
|
||||
} from './transfer'
|
||||
import type { TransferPanelInstance } from './transfer-panel'
|
||||
|
||||
defineOptions({
|
||||
name: 'ElTransfer',
|
||||
})
|
||||
|
||||
const props = defineProps(transferProps)
|
||||
const emit = defineEmits(transferEmits)
|
||||
const slots = useSlots()
|
||||
|
||||
const { t } = useLocale()
|
||||
const ns = useNamespace('transfer')
|
||||
const { formItem } = useFormItem()
|
||||
|
||||
const checkedState = reactive<TransferCheckedState>({
|
||||
leftChecked: [],
|
||||
rightChecked: [],
|
||||
})
|
||||
|
||||
const propsAlias = usePropsAlias(props)
|
||||
|
||||
const { sourceData, targetData } = useComputedData(props)
|
||||
|
||||
const { onSourceCheckedChange, onTargetCheckedChange } = useCheckedChange(
|
||||
checkedState,
|
||||
emit
|
||||
)
|
||||
|
||||
const { addToLeft, addToRight } = useMove(props, checkedState, emit)
|
||||
|
||||
const leftPanel = ref<TransferPanelInstance>()
|
||||
const rightPanel = ref<TransferPanelInstance>()
|
||||
|
||||
const clearQuery = (which: TransferDirection) => {
|
||||
switch (which) {
|
||||
case 'left':
|
||||
leftPanel.value!.query = ''
|
||||
break
|
||||
case 'right':
|
||||
rightPanel.value!.query = ''
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const hasButtonTexts = computed(() => props.buttonTexts.length === 2)
|
||||
|
||||
const leftPanelTitle = computed(
|
||||
() => props.titles[0] || t('el.transfer.titles.0')
|
||||
)
|
||||
|
||||
const rightPanelTitle = computed(
|
||||
() => props.titles[1] || t('el.transfer.titles.1')
|
||||
)
|
||||
|
||||
const panelFilterPlaceholder = computed(
|
||||
() => props.filterPlaceholder || t('el.transfer.filterPlaceholder')
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
formItem?.validate?.('change').catch((err) => debugWarn(err))
|
||||
}
|
||||
)
|
||||
|
||||
const optionRender = computed(() => (option: TransferDataItem) => {
|
||||
if (props.renderContent) return props.renderContent(h, option)
|
||||
|
||||
if (slots.default) return slots.default({ option })
|
||||
|
||||
return h(
|
||||
'span',
|
||||
option[propsAlias.value.label] || option[propsAlias.value.key]
|
||||
)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
/** @description clear the filter keyword of a certain panel */
|
||||
clearQuery,
|
||||
/** @description left panel ref */
|
||||
leftPanel,
|
||||
/** @description left panel ref */
|
||||
rightPanel,
|
||||
})
|
||||
</script>
|
@ -1,23 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import type { Key, TransferCheckedState } from './transfer'
|
||||
|
||||
export const LEFT_CHECK_CHANGE_EVENT = 'left-check-change'
|
||||
export const RIGHT_CHECK_CHANGE_EVENT = 'right-check-change'
|
||||
export const useCheckedChange = (checkedState: TransferCheckedState, emit) => {
|
||||
const onSourceCheckedChange = (val: Key[], movedKeys: Key[]) => {
|
||||
checkedState.leftChecked = val
|
||||
if (movedKeys === undefined) return
|
||||
emit(LEFT_CHECK_CHANGE_EVENT, val, movedKeys)
|
||||
}
|
||||
|
||||
const onTargetCheckedChange = (val: Key[], movedKeys: Key[]) => {
|
||||
checkedState.rightChecked = val
|
||||
if (movedKeys === undefined) return
|
||||
emit(RIGHT_CHECK_CHANGE_EVENT, val, movedKeys)
|
||||
}
|
||||
|
||||
return {
|
||||
onSourceCheckedChange,
|
||||
onTargetCheckedChange,
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { TransferProps } from './transfer'
|
||||
|
||||
export const useComputedData = (props: TransferProps) => {
|
||||
const propsKey = computed(() => props.props.key)
|
||||
|
||||
const dataObj = computed(() => {
|
||||
return props.data.reduce(
|
||||
(o, cur) => (o[cur[propsKey.value]] = cur) && o,
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
const sourceData = computed(() => {
|
||||
return props.data.filter(
|
||||
(item) => !props.modelValue.includes(item[propsKey.value])
|
||||
)
|
||||
})
|
||||
|
||||
const targetData = computed(() => {
|
||||
if (props.targetOrder === 'original') {
|
||||
return props.data.filter((item) =>
|
||||
props.modelValue.includes(item[propsKey.value])
|
||||
)
|
||||
} else {
|
||||
return props.modelValue.reduce((arr, cur) => {
|
||||
const val = dataObj.value[cur]
|
||||
if (val) {
|
||||
arr.push(val)
|
||||
}
|
||||
return arr
|
||||
}, [])
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
propsKey,
|
||||
sourceData,
|
||||
targetData,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user