feat(transfer): redesign

This commit is contained in:
07akioni 2022-07-24 22:14:15 +08:00
parent 3b64ef8fba
commit 2cf2a28213
74 changed files with 1242 additions and 1093 deletions

View File

@ -2,6 +2,10 @@
## NEXT_VERSION
### Breaking Changes
- `n-transfer`'s UI is totally refactored. The original transfer component is renamed as `n-legacy-transfer` and will be removed in next major version.
### Fixes
- `n-notification` add `keepAliveOnHover` props to control whether the notification will be closed when mouse hover, closes [#3249](https://github.com/TuSimple/naive-ui/issues/3249).
@ -20,7 +24,8 @@
- `n-checkbox-group`'s `on-update:value` prop adds trigger checkbox's value to params, closes [#3277](https://github.com/TuSimple/naive-ui/issues/3277).
- `n-tree` supports RTL.
- `n-input` adds `scrollTo` method, closes [#3280](https://github.com/TuSimple/naive-ui/issues/3280).
- `n-transfer` add `render-label` prop.
- `n-transfer` add `render-source-label` prop.
- `n-transfer` add `render-target-label` prop.
- `n-transfer` add `render-source-list` prop.
## 2.31.0

View File

@ -4,7 +4,7 @@
### Breaking Changes
- `n-transfer`样式及代码被重构
- `n-transfer` UI 完全重构,原本的 transfer 组件被重命名为 `n-legacy-transfer`,并将在下个主版本被移除。
### Fixes
@ -24,7 +24,8 @@
- `n-checkbox-group``on-update:value` 属性增加触发变更的 checkbox 的值到参数中,关闭 [#3277](https://github.com/TuSimple/naive-ui/issues/3277)
- `n-tree` 支持 RTL
- `n-input` 新增 `scrollTo` 方法,关闭 [#3280](https://github.com/TuSimple/naive-ui/issues/3280)
- `n-transfer` 新增 `render-label` 属性
- `n-transfer` 新增 `render-source-label` 属性
- `n-transfer` 新增 `render-target-label` 属性
- `n-transfer` 新增 `render-source-list` 属性
## 2.31.0

View File

@ -44,6 +44,7 @@ export * from './input'
export * from './input-number'
export * from './layout'
export * from './legacy-grid'
export * from './legacy-transfer'
export * from './list'
export * from './loading-bar'
export * from './log'

View File

@ -37,6 +37,7 @@ import type { ImageTheme } from '../../image/styles'
import type { InputTheme } from '../../input/styles'
import type { InputNumberTheme } from '../../input-number/styles'
import type { LayoutTheme } from '../../layout/styles'
import type { LegacyTransferTheme } from '../../legacy-transfer/styles'
import type { ListTheme } from '../../list/styles'
import type { LoadingBarTheme } from '../../loading-bar/styles'
import type { LogTheme } from '../../log/styles'
@ -134,6 +135,7 @@ export interface GlobalThemeWithoutCommon {
Input?: InputTheme
InputNumber?: InputNumberTheme
Layout?: LayoutTheme
LegacyTransfer?: LegacyTransferTheme
List?: ListTheme
LoadingBar?: LoadingBarTheme
Log?: LogTheme

View File

@ -1,11 +1,11 @@
<markdown>
# Basic
Basic example of the Transfer component. If you have tons of data, see below for virtualised lists.
Basic example of the Transfer component. If you have tons of data, see below for virtualized list.
</markdown>
<template>
<n-transfer ref="transfer" v-model:value="value" :options="options" />
<n-legacy-transfer ref="transfer" v-model:value="value" :options="options" />
</template>
<script lang="ts">

View File

@ -3,7 +3,7 @@
</markdown>
<template>
<n-transfer
<n-legacy-transfer
ref="transfer"
v-model:value="value"
virtual-scroll

View File

@ -2,8 +2,10 @@
<!--single-column-->
<n-alert title="Warning">
The transfer component is deprecated and will be removed in the next major version.
<n-alert title="Warning" type="warning">
The transfer component is deprecated. It won't have any new feature and will be removed in the next major version. It's recommended to use new <router-link to="transfer" custom v-slot="{ href, navigate }">
<n-a :href="href" @click="navigate">Transfer</n-a>
</router-link>.
</n-alert>
Left, right, right, left... I'm a simple man, and I can play this all day.
@ -27,7 +29,7 @@ filterable.vue
| disabled | `boolean` | `true` | Disabled state. |
| filterable | `boolean` | `false` | Filterable state. |
| filter | `function` | `(pattern: string, option: TransferOption, from: 'source' \| 'target') => boolean` | A basic label string match function. |
| options | `Array<TransferOption>` | `[]` | For configuration options, see the TransferOption Type below. |
| options | `TransferOption[]` | `[]` | For configuration options, see the TransferOption Type below. |
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | Size. |
| source-filter-placeholder | `string` | `undefined` | Placeholder for the source items search box. |
| source-title | `string` | `'Source'` | Source items title. |

View File

@ -5,7 +5,7 @@ If you have tons of data, you may need to speed the transfer up! Set `virtual-sc
</markdown>
<template>
<n-transfer
<n-legacy-transfer
ref="transfer"
v-model:value="value"
:options="options"

View File

@ -6,13 +6,13 @@ Mixing sizes does not look harmonious.
<template>
<n-space vertical>
<n-transfer
<n-legacy-transfer
ref="transfer"
v-model:value="value"
:options="options"
size="small"
/>
<n-transfer
<n-legacy-transfer
ref="transfer"
v-model:value="value"
:options="options"

View File

@ -5,7 +5,7 @@
</markdown>
<template>
<n-transfer ref="transfer" v-model:value="value" :options="options" />
<n-legacy-transfer ref="transfer" v-model:value="value" :options="options" />
</template>
<script lang="ts">

View File

@ -3,7 +3,7 @@
</markdown>
<template>
<n-transfer
<n-legacy-transfer
ref="transfer"
v-model:value="value"
virtual-scroll

View File

@ -2,8 +2,10 @@
<!--single-column-->
<n-alert title="警告">
这个穿梭框组件已经被废弃,并将在下一个大版本中彻底移除。
<n-alert title="警告" type="warning">
这个穿梭框组件已经被废弃,不会迭代任何新的功能,并将在下一个大版本中彻底移除。推荐使用新的 <router-link to="transfer" custom v-slot="{ href, navigate }">
<n-a :href="href" @click="navigate">穿梭框</n-a>
</router-link>
</n-alert>
左、右、左、右...像我这么无聊的人能玩一整天。
@ -27,7 +29,7 @@ filterable.vue
| disabled | `boolean` | `true` | 是否禁用 |
| filterable | `boolean` | `false` | 是否可过滤 |
| filter | `(pattern: string, option: TransferOption, from: 'source' \| 'target') => boolean` | 一个简单的标签字符串匹配函数 | 搜索时使用的过滤函数 |
| options | `Array<TransferOption>` | `[]` | 配置选项内容,详情见 TransferOption Type |
| options | `TransferOption[]` | `[]` | 配置选项内容,详情见 TransferOption Type |
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | 尺寸 |
| source-filter-placeholder | `string` | `undefined` | 源项搜索框中的占位符 |
| source-title | `string` | `'源项'` | 源项标题 |

View File

@ -5,7 +5,7 @@
</markdown>
<template>
<n-transfer
<n-legacy-transfer
ref="transfer"
v-model:value="value"
:options="options"

View File

@ -6,13 +6,13 @@
<template>
<n-space vertical>
<n-transfer
<n-legacy-transfer
ref="transfer"
v-model:value="value"
:options="options"
size="small"
/>
<n-transfer
<n-legacy-transfer
ref="transfer"
v-model:value="value"
:options="options"

View File

@ -4,7 +4,8 @@ import {
h,
provide,
PropType,
CSSProperties
CSSProperties,
watchEffect
} from 'vue'
import { useIsMounted } from 'vooks'
import { depx } from 'seemly'
@ -14,10 +15,10 @@ import { NButton } from '../../button'
import { useLocale, useFormItem, useTheme, useConfig } from '../../_mixins'
import type { ThemeProps } from '../../_mixins'
import { createKey } from '../../_utils/cssr'
import { warn, call, ExtractPublicPropTypes } from '../../_utils'
import { call, ExtractPublicPropTypes, warnOnce } from '../../_utils'
import type { MaybeArray } from '../../_utils'
import { transferLight } from '../styles'
import type { TransferTheme } from '../styles'
import { legacyTransferLight } from '../styles'
import type { LegacyTransferTheme } from '../styles'
import NTransferHeader from './TransferHeader'
import NTransferList from './TransferList'
import NTransferFilter from './TransferFilter'
@ -32,7 +33,7 @@ import {
} from './interface'
export const transferProps = {
...(useTheme.props as ThemeProps<TransferTheme>),
...(useTheme.props as ThemeProps<LegacyTransferTheme>),
value: Array as PropType<OptionValue[] | null>,
defaultValue: {
type: Array as PropType<OptionValue[] | null>,
@ -64,33 +65,31 @@ export const transferProps = {
size: String as PropType<'small' | 'medium' | 'large'>,
'onUpdate:value': [Function, Array] as PropType<MaybeArray<OnUpdateValue>>,
onUpdateValue: [Function, Array] as PropType<MaybeArray<OnUpdateValue>>,
onChange: {
type: [Function, Array] as PropType<MaybeArray<OnUpdateValue>>,
validator: () => {
if (__DEV__) {
warn(
'transfer',
'`on-change` is deprecated, please use `on-update:value` instead.'
)
}
return true
},
default: undefined
}
onChange: [Function, Array] as PropType<MaybeArray<OnUpdateValue>>
} as const
export type TransferProps = ExtractPublicPropTypes<typeof transferProps>
export default defineComponent({
name: 'Transfer',
name: 'LegacyTransfer',
props: transferProps,
setup (props) {
if (__DEV__) {
watchEffect(() => {
if (props.onChange !== undefined) {
warnOnce(
'legacy-transfer',
'`on-change` is deprecated, please use `on-update:value` instead.'
)
}
})
}
const { mergedClsPrefixRef } = useConfig(props)
const themeRef = useTheme(
'Transfer',
'-transfer',
'LegacyTransfer',
'-legacy-transfer',
style,
transferLight,
legacyTransferLight,
props,
mergedClsPrefixRef
)
@ -217,7 +216,7 @@ export default defineComponent({
handleSrcCheckboxClick,
handleTgtCheckboxClick
})
const { localeRef } = useLocale('Transfer')
const { localeRef } = useLocale('LegacyTransfer')
return {
locale: localeRef,
mergedClsPrefix: mergedClsPrefixRef,
@ -300,19 +299,19 @@ export default defineComponent({
return (
<div
class={[
`${mergedClsPrefix}-transfer`,
this.mergedDisabled && `${mergedClsPrefix}-transfer--disabled`,
this.filterable && `${mergedClsPrefix}-transfer--filterable`
`${mergedClsPrefix}-legacy-transfer`,
this.mergedDisabled && `${mergedClsPrefix}-legacy-transfer--disabled`,
this.filterable && `${mergedClsPrefix}-legacy-transfer--filterable`
]}
style={this.cssVars as CSSProperties}
>
<div class={`${mergedClsPrefix}-transfer-list`}>
<div class={`${mergedClsPrefix}-legacy-transfer-list`}>
<NTransferHeader
source
onChange={this.handleSrcHeaderCheck}
title={this.sourceTitle || this.locale.sourceTitle}
/>
<div class={`${mergedClsPrefix}-transfer-list-body`}>
<div class={`${mergedClsPrefix}-legacy-transfer-list-body`}>
{this.filterable ? (
<NTransferFilter
onUpdateValue={this.handleSrcFilterUpdateValue}
@ -323,7 +322,9 @@ export default defineComponent({
onBlur={this.handleInputBlur}
/>
) : null}
<div class={`${mergedClsPrefix}-transfer-list-flex-container`}>
<div
class={`${mergedClsPrefix}-legacy-transfer-list-flex-container`}
>
<NTransferList
source
options={this.filteredSrcOpts}
@ -335,9 +336,9 @@ export default defineComponent({
/>
</div>
</div>
<div class={`${mergedClsPrefix}-transfer-list__border`} />
<div class={`${mergedClsPrefix}-legacy-transfer-list__border`} />
</div>
<div class={`${mergedClsPrefix}-transfer-gap`}>
<div class={`${mergedClsPrefix}-legacy-transfer-gap`}>
<NButton
disabled={this.toButtonDisabled || this.mergedDisabled}
theme={this.mergedTheme.peers.Button}
@ -367,12 +368,12 @@ export default defineComponent({
}}
</NButton>
</div>
<div class={`${mergedClsPrefix}-transfer-list`}>
<div class={`${mergedClsPrefix}-legacy-transfer-list`}>
<NTransferHeader
onChange={this.handleTgtHeaderCheck}
title={this.targetTitle || this.locale.targetTitle}
/>
<div class={`${mergedClsPrefix}-transfer-list-body`}>
<div class={`${mergedClsPrefix}-legacy-transfer-list-body`}>
{this.filterable ? (
<NTransferFilter
onUpdateValue={this.handleTgtFilterUpdateValue}
@ -383,7 +384,9 @@ export default defineComponent({
onBlur={this.handleInputBlur}
/>
) : null}
<div class={`${mergedClsPrefix}-transfer-list-flex-container`}>
<div
class={`${mergedClsPrefix}-legacy-transfer-list-flex-container`}
>
<NTransferList
options={this.filteredTgtOpts}
disabled={this.mergedDisabled}
@ -394,7 +397,7 @@ export default defineComponent({
/>
</div>
</div>
<div class={`${mergedClsPrefix}-transfer-list__border`} />
<div class={`${mergedClsPrefix}-legacy-transfer-list__border`} />
</div>
</div>
)

View File

@ -34,7 +34,7 @@ export default defineComponent({
render () {
const { mergedTheme, mergedClsPrefix } = this
return (
<div class={`${mergedClsPrefix}-transfer-filter`}>
<div class={`${mergedClsPrefix}-legacy-transfer-filter`}>
<NInput
value={this.value}
onUpdateValue={this.onUpdateValue}
@ -51,7 +51,7 @@ export default defineComponent({
'clear-icon-placeholder': () => (
<NBaseIcon
clsPrefix={mergedClsPrefix}
class={`${mergedClsPrefix}-transfer-icon`}
class={`${mergedClsPrefix}-legacy-transfer-icon`}
>
{{ default: () => <SearchIcon /> }}
</NBaseIcon>

View File

@ -42,8 +42,10 @@ export default defineComponent({
const { value: mergedTheme } = mergedThemeRef
const { value: mergedClsPrefix } = mergedClsPrefixRef
return (
<div class={`${mergedClsPrefix}-transfer-list-header`}>
<div class={`${mergedClsPrefix}-transfer-list-header__checkbox`}>
<div class={`${mergedClsPrefix}-legacy-transfer-list-header`}>
<div
class={`${mergedClsPrefix}-legacy-transfer-list-header__checkbox`}
>
<NCheckbox
theme={mergedTheme.peers.Checkbox}
themeOverrides={mergedTheme.peerOverrides.Checkbox}
@ -53,10 +55,10 @@ export default defineComponent({
onUpdateChecked={props.onChange}
/>
</div>
<div class={`${mergedClsPrefix}-transfer-list-header__header`}>
<div class={`${mergedClsPrefix}-legacy-transfer-list-header__header`}>
{props.title}
</div>
<div class={`${mergedClsPrefix}-transfer-list-header__extra`}>
<div class={`${mergedClsPrefix}-legacy-transfer-list-header__extra`}>
{source
? srcCheckedValuesRef.value.length
: tgtCheckedValuesRef.value.length}

View File

@ -93,7 +93,7 @@ export default defineComponent({
<VirtualList
ref="vlInstRef"
style={{ height: '100%' }}
class={`${mergedClsPrefix}-transfer-list-content`}
class={`${mergedClsPrefix}-legacy-transfer-list-content`}
items={this.options}
itemSize={this.itemSize}
showScrollbar={false}
@ -117,7 +117,7 @@ export default defineComponent({
}}
</VirtualList>
) : (
<div class={`${mergedClsPrefix}-transfer-list-content`}>
<div class={`${mergedClsPrefix}-legacy-transfer-list-content`}>
<TransitionGroup
name="item"
appear={this.isMounted}

View File

@ -62,15 +62,15 @@ export default defineComponent({
return (
<div
class={[
`${mergedClsPrefix}-transfer-list-item`,
disabled && `${mergedClsPrefix}-transfer-list-item--disabled`,
`${mergedClsPrefix}-legacy-transfer-list-item`,
disabled && `${mergedClsPrefix}-legacy-transfer-list-item--disabled`,
source
? `${mergedClsPrefix}-transfer-list-item--source`
: `${mergedClsPrefix}-transfer-list-item--target`
? `${mergedClsPrefix}-legacy-transfer-list-item--source`
: `${mergedClsPrefix}-legacy-transfer-list-item--target`
]}
onClick={this.handleClick}
>
<div class={`${mergedClsPrefix}-transfer-list-item__checkbox`}>
<div class={`${mergedClsPrefix}-legacy-transfer-list-item__checkbox`}>
<NCheckbox
theme={mergedTheme.peers.Checkbox}
themeOverrides={mergedTheme.peerOverrides.Checkbox}
@ -79,7 +79,7 @@ export default defineComponent({
/>
</div>
<div
class={`${mergedClsPrefix}-transfer-list-item__label`}
class={`${mergedClsPrefix}-legacy-transfer-list-item__label`}
title={getTitleAttribute(label)}
>
{label}

View File

@ -1,7 +1,7 @@
import { Ref } from 'vue'
import type { MergedTheme } from '../../_mixins'
import { createInjectionKey } from '../../_utils'
import type { TransferTheme } from '../styles'
import type { LegacyTransferTheme } from '../styles'
export type OptionValue = string | number
export interface Option {
@ -26,7 +26,7 @@ export interface TransferInjection {
mergedClsPrefixRef: Ref<string>
mergedSizeRef: Ref<'small' | 'medium' | 'large'>
disabledRef: Ref<boolean>
mergedThemeRef: Ref<MergedTheme<TransferTheme>>
mergedThemeRef: Ref<MergedTheme<LegacyTransferTheme>>
srcCheckedValuesRef: Ref<OptionValue[]>
tgtCheckedValuesRef: Ref<OptionValue[]>
srcOptsRef: Ref<Option[]>

View File

@ -2,7 +2,7 @@ import { c, cB, cE, cM, cNotM } from '../../../_utils/cssr'
import { fadeInTransition } from '../../../_styles/transitions/fade-in.cssr'
const animation = c([
c('@keyframes transfer-slide-in-from-left', `
c('@keyframes legacy-transfer-slide-in-from-left', `
0% {
transform: translateX(-150%);
}
@ -10,7 +10,7 @@ const animation = c([
transform: translateX(0);
}
`),
c('@keyframes transfer-slide-out-to-right', `
c('@keyframes legacy-transfer-slide-out-to-right', `
0% {
transform: translateX(0);
}
@ -18,7 +18,7 @@ const animation = c([
transform: translateX(150%);
}
`),
c('@keyframes transfer-slide-in-from-right', `
c('@keyframes legacy-transfer-slide-in-from-right', `
0% {
transform: translateX(150%);
}
@ -26,7 +26,7 @@ const animation = c([
transform: translateX(0);
}
`),
c('@keyframes transfer-slide-out-to-left', `
c('@keyframes legacy-transfer-slide-out-to-left', `
0% {
transform: translateX(0);
}
@ -34,7 +34,7 @@ const animation = c([
transform: translateX(-150%);
}
`),
c('@keyframes transfer-height-collapse', `
c('@keyframes legacy-transfer-height-collapse', `
0% {
max-height: var(--n-item-height);
}
@ -42,7 +42,7 @@ const animation = c([
max-height: 0;
}
`),
c('@keyframes transfer-height-expand', `
c('@keyframes legacy-transfer-height-expand', `
0% {
max-height: 0;
}
@ -53,7 +53,7 @@ const animation = c([
])
export default c([
cB('transfer', `
cB('legacy-transfer', `
display: flex;
width: var(--n-width);
font-size: var(--n-font-size);
@ -61,16 +61,16 @@ export default c([
display: flex;
flex-wrap: nowrap;
`, [
cB('transfer-icon', `
cB('legacy-transfer-icon', `
color: var(--n-icon-color);
transition: color .3s var(--n-bezier);
`),
cM('disabled', [
cB('transfer-icon', {
cB('legacy-transfer-icon', {
color: 'var(--n-icon-color-disabled)'
})
]),
cB('transfer-list', `
cB('legacy-transfer-list', `
height: inherit;
display: flex;
flex-direction: column;
@ -92,7 +92,7 @@ export default c([
top: 0;
bottom: 0;
`),
cB('transfer-list-header', `
cB('legacy-transfer-list-header', `
height: calc(var(--n-item-height) + 4px);
box-sizing: border-box;
display: flex;
@ -132,7 +132,7 @@ export default c([
color: var(--n-header-extra-text-color);
`)
]),
cB('transfer-list-body', `
cB('legacy-transfer-list-body', `
flex-basis: 0;
flex-grow: 1;
box-sizing: border-box;
@ -143,7 +143,7 @@ export default c([
border-top-left-radius: 0;
border-top-right-radius: 0;
`, [
cB('transfer-filter', `
cB('legacy-transfer-filter', `
padding: 0 8px 8px 8px;
box-sizing: border-box;
background-color: var(--n-header-color);
@ -152,7 +152,7 @@ export default c([
background-color .3s var(--n-bezier);
border-bottom: 1px solid var(--n-filter-divider-color);
`),
cB('transfer-list-flex-container', `
cB('legacy-transfer-list-flex-container', `
flex: 1;
position: relative;
`, [
@ -176,17 +176,17 @@ export default c([
`, [
fadeInTransition()
]),
cB('transfer-list-content', `
cB('legacy-transfer-list-content', `
padding: 0;
margin: 0;
position: relative;
`, [
cM('transition-disabled', [
cB('transfer-list-item', {
cB('legacy-transfer-list-item', {
animation: 'none !important'
})
]),
cB('transfer-list-item', `
cB('legacy-transfer-list-item', `
height: var(--n-item-height);
max-height: var(--n-item-height);
transition:
@ -228,14 +228,14 @@ export default c([
animation-duration: .25s, .25s;
animation-timing-function: var(--n-bezier), var(--n-bezier-ease-out);
animation-delay: 0s, .25s;
animation-name: transfer-height-expand, transfer-slide-in-from-right;
animation-name: legacy-transfer-height-expand, legacy-transfer-slide-in-from-right;
`),
c('&.item-leave-active', `
transform: translateX(-150%);
animation-duration: .25s, .25s;
animation-timing-function: var(--n-bezier), var(--n-bezier-ease-in);
animation-delay: .25s, 0s;
animation-name: transfer-height-collapse, transfer-slide-out-to-right;
animation-name: legacy-transfer-height-collapse, legacy-transfer-slide-out-to-right;
`)
]),
cM('target', {
@ -246,14 +246,14 @@ export default c([
animation-duration: .25s, .25s;
animation-timing-function: var(--n-bezier), var(--n-bezier-ease-out);
animation-delay: 0s, .25s;
animation-name: transfer-height-expand, transfer-slide-in-from-left;
animation-name: legacy-transfer-height-expand, legacy-transfer-slide-in-from-left;
`),
c('&.item-leave-active', `
transform: translateX(150%);
animation-duration: .25s, .25s;
animation-timing-function: var(--n-bezier), var(--n-bezier-ease-in);
animation-delay: .25s, 0s;
animation-name: transfer-height-collapse, transfer-slide-out-to-left;
animation-name: legacy-transfer-height-collapse, legacy-transfer-slide-out-to-left;
`)
])
])
@ -261,7 +261,7 @@ export default c([
])
])
]),
cB('transfer-gap', {
cB('legacy-transfer-gap', {
width: '72px',
display: 'flex',
alignItems: 'center',

View File

@ -1,3 +1,6 @@
export { default as transferDark } from './dark'
export { default as transferLight } from './light'
export type { TransferTheme, TransferThemeVars } from './light'
export { default as legacyTransferDark } from './dark'
export { default as legacyTransferLight } from './light'
export type {
TransferTheme as LegacyTransferTheme,
TransferThemeVars as LegacyTransferThemeVars
} from './light'

View File

@ -1,23 +1,23 @@
import { mount } from '@vue/test-utils'
import { sleep } from 'seemly'
import { NTransfer } from '../index'
import { NLegacyTransfer } from '../index'
describe('n-transfer', () => {
describe('n-legacy-transfer', () => {
it('should work with import on demand', () => {
mount(NTransfer)
mount(NLegacyTransfer)
})
it('should work with `disabled` prop', () => {
const wrapper = mount(NTransfer, { props: { disabled: true } })
expect(wrapper.find('.n-transfer').attributes('class')).toContain(
'n-transfer--disabled'
const wrapper = mount(NLegacyTransfer, { props: { disabled: true } })
expect(wrapper.find('.n-legacy-transfer').attributes('class')).toContain(
'n-legacy-transfer--disabled'
)
})
it('should work with `filterable` prop', () => {
const wrapper = mount(NTransfer, { props: { filterable: true } })
expect(wrapper.find('.n-transfer').attributes('class')).toContain(
'n-transfer--filterable'
const wrapper = mount(NLegacyTransfer, { props: { filterable: true } })
expect(wrapper.find('.n-legacy-transfer').attributes('class')).toContain(
'n-legacy-transfer--filterable'
)
})
@ -29,7 +29,7 @@ describe('n-transfer', () => {
}
]
const onFilter = jest.fn()
const wrapper = mount(NTransfer, {
const wrapper = mount(NLegacyTransfer, {
props: { filterable: true, filter: onFilter, options: options }
})
await wrapper.find('input').setValue('1')
@ -39,15 +39,17 @@ describe('n-transfer', () => {
it('should work with `size` prop', async () => {
;(['small', 'medium', 'large'] as const).forEach((i) => {
const wrapper = mount(NTransfer, {
const wrapper = mount(NLegacyTransfer, {
props: { size: i }
})
expect(wrapper.find('.n-transfer').attributes('style')).toMatchSnapshot()
expect(
wrapper.find('.n-legacy-transfer').attributes('style')
).toMatchSnapshot()
})
})
it('should work with `source-filter-placeholder`、`target-filter-placeholder` props', async () => {
const wrapper = mount(NTransfer, {
const wrapper = mount(NLegacyTransfer, {
props: {
filterable: true,
'source-filter-placeholder': 'test-source',
@ -63,17 +65,17 @@ describe('n-transfer', () => {
})
it('should work with `source-title`、`target-title` props', async () => {
const wrapper = mount(NTransfer, {
const wrapper = mount(NLegacyTransfer, {
props: {
'source-title': 'test-source',
'target-title': 'test-target'
}
})
expect(wrapper.findAll('.n-transfer-list-header__header')[0].text()).toBe(
'test-source'
)
expect(wrapper.findAll('.n-transfer-list-header__header')[1].text()).toBe(
'test-target'
)
expect(
wrapper.findAll('.n-legacy-transfer-list-header__header')[0].text()
).toBe('test-source')
expect(
wrapper.findAll('.n-legacy-transfer-list-header__header')[1].text()
).toBe('test-target')
})
})

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`n-transfer should work with \`size\` prop 1`] = `"--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-bezier-ease-in: cubic-bezier(.4, 0, 1, 1); --n-bezier-ease-out: cubic-bezier(0, 0, .2, 1); --n-border-color: rgb(224, 224, 230); --n-border-radius: 3px; --n-extra-font-size: 12px; --n-filter-divider-color: rgb(224, 224, 230); --n-font-size: 14px; --n-header-color: rgba(250, 250, 252, 1); --n-header-extra-text-color: rgb(51, 54, 57); --n-header-font-weight: 400; --n-header-text-color: rgb(31, 34, 37); --n-header-text-color-disabled: rgba(194, 194, 194, 1); --n-item-color-pending: rgb(243, 243, 245); --n-item-height: 28px; --n-item-text-color: rgb(51, 54, 57); --n-item-text-color-disabled: rgba(194, 194, 194, 1); --n-list-color: #fff; --n-width: 440px; --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1);"`;
exports[`n-legacy-transfer should work with \`size\` prop 1`] = `"--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-bezier-ease-in: cubic-bezier(.4, 0, 1, 1); --n-bezier-ease-out: cubic-bezier(0, 0, .2, 1); --n-border-color: rgb(224, 224, 230); --n-border-radius: 3px; --n-extra-font-size: 12px; --n-filter-divider-color: rgb(224, 224, 230); --n-font-size: 14px; --n-header-color: rgba(250, 250, 252, 1); --n-header-extra-text-color: rgb(51, 54, 57); --n-header-font-weight: 400; --n-header-text-color: rgb(31, 34, 37); --n-header-text-color-disabled: rgba(194, 194, 194, 1); --n-item-color-pending: rgb(243, 243, 245); --n-item-height: 28px; --n-item-text-color: rgb(51, 54, 57); --n-item-text-color-disabled: rgba(194, 194, 194, 1); --n-list-color: #fff; --n-width: 440px; --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1);"`;
exports[`n-transfer should work with \`size\` prop 2`] = `"--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-bezier-ease-in: cubic-bezier(.4, 0, 1, 1); --n-bezier-ease-out: cubic-bezier(0, 0, .2, 1); --n-border-color: rgb(224, 224, 230); --n-border-radius: 3px; --n-extra-font-size: 12px; --n-filter-divider-color: rgb(224, 224, 230); --n-font-size: 14px; --n-header-color: rgba(250, 250, 252, 1); --n-header-extra-text-color: rgb(51, 54, 57); --n-header-font-weight: 400; --n-header-text-color: rgb(31, 34, 37); --n-header-text-color-disabled: rgba(194, 194, 194, 1); --n-item-color-pending: rgb(243, 243, 245); --n-item-height: 34px; --n-item-text-color: rgb(51, 54, 57); --n-item-text-color-disabled: rgba(194, 194, 194, 1); --n-list-color: #fff; --n-width: 440px; --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1);"`;
exports[`n-legacy-transfer should work with \`size\` prop 2`] = `"--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-bezier-ease-in: cubic-bezier(.4, 0, 1, 1); --n-bezier-ease-out: cubic-bezier(0, 0, .2, 1); --n-border-color: rgb(224, 224, 230); --n-border-radius: 3px; --n-extra-font-size: 12px; --n-filter-divider-color: rgb(224, 224, 230); --n-font-size: 14px; --n-header-color: rgba(250, 250, 252, 1); --n-header-extra-text-color: rgb(51, 54, 57); --n-header-font-weight: 400; --n-header-text-color: rgb(31, 34, 37); --n-header-text-color-disabled: rgba(194, 194, 194, 1); --n-item-color-pending: rgb(243, 243, 245); --n-item-height: 34px; --n-item-text-color: rgb(51, 54, 57); --n-item-text-color-disabled: rgba(194, 194, 194, 1); --n-list-color: #fff; --n-width: 440px; --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1);"`;
exports[`n-transfer should work with \`size\` prop 3`] = `"--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-bezier-ease-in: cubic-bezier(.4, 0, 1, 1); --n-bezier-ease-out: cubic-bezier(0, 0, .2, 1); --n-border-color: rgb(224, 224, 230); --n-border-radius: 3px; --n-extra-font-size: 12px; --n-filter-divider-color: rgb(224, 224, 230); --n-font-size: 15px; --n-header-color: rgba(250, 250, 252, 1); --n-header-extra-text-color: rgb(51, 54, 57); --n-header-font-weight: 400; --n-header-text-color: rgb(31, 34, 37); --n-header-text-color-disabled: rgba(194, 194, 194, 1); --n-item-color-pending: rgb(243, 243, 245); --n-item-height: 40px; --n-item-text-color: rgb(51, 54, 57); --n-item-text-color-disabled: rgba(194, 194, 194, 1); --n-list-color: #fff; --n-width: 440px; --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1);"`;
exports[`n-legacy-transfer should work with \`size\` prop 3`] = `"--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-bezier-ease-in: cubic-bezier(.4, 0, 1, 1); --n-bezier-ease-out: cubic-bezier(0, 0, .2, 1); --n-border-color: rgb(224, 224, 230); --n-border-radius: 3px; --n-extra-font-size: 12px; --n-filter-divider-color: rgb(224, 224, 230); --n-font-size: 15px; --n-header-color: rgba(250, 250, 252, 1); --n-header-extra-text-color: rgb(51, 54, 57); --n-header-font-weight: 400; --n-header-text-color: rgb(31, 34, 37); --n-header-text-color-disabled: rgba(194, 194, 194, 1); --n-item-color-pending: rgb(243, 243, 245); --n-item-height: 40px; --n-item-text-color: rgb(51, 54, 57); --n-item-text-color-disabled: rgba(194, 194, 194, 1); --n-list-color: #fff; --n-width: 440px; --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1);"`;

View File

@ -57,9 +57,17 @@ const deDE: NLocale = {
confirm: 'Bestätigen',
clear: 'Löschen'
},
LegacyTransfer: {
sourceTitle: 'Quelle',
targetTitle: 'Ziel'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Keine Daten'

View File

@ -56,9 +56,16 @@ const enGB: NLocale = {
confirm: 'Confirm',
clear: 'Clear'
},
LegacyTransfer: {
sourceTitle: 'Source',
targetTitle: 'Target'
},
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'No Data'

View File

@ -54,9 +54,16 @@ const enUS = {
confirm: 'Confirm',
clear: 'Clear'
},
LegacyTransfer: {
sourceTitle: 'Source',
targetTitle: 'Target'
},
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'No Data'

View File

@ -57,9 +57,17 @@ const eo: NLocale = {
confirm: 'Konfirmi',
clear: 'Malplenigi'
},
LegacyTransfer: {
sourceTitle: 'Source',
targetTitle: 'Target'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Neniu datumo'

View File

@ -58,9 +58,17 @@ const esAR: NLocale = {
confirm: 'Confirmar',
clear: 'Limpiar'
},
LegacyTransfer: {
sourceTitle: 'Fuente',
targetTitle: 'Objetivo'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Sin datos'

View File

@ -57,9 +57,17 @@ const frFR: NLocale = {
confirm: 'Confirmer',
clear: 'Effacer'
},
LegacyTransfer: {
sourceTitle: 'Source',
targetTitle: 'Cible'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Aucune donnée'

View File

@ -58,9 +58,17 @@ const idID: NLocale = {
confirm: 'Setuju',
clear: 'Bersihkan'
},
LegacyTransfer: {
sourceTitle: 'Sumber',
targetTitle: 'Tujuan'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Tidak ada data'

View File

@ -57,9 +57,17 @@ const itIT: NLocale = {
confirm: 'Conferma',
clear: 'Cancella'
},
LegacyTransfer: {
sourceTitle: 'Fonte',
targetTitle: 'Destinazione'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Nessun Dato'

View File

@ -57,9 +57,17 @@ const jaJP: NLocale = {
confirm: 'OK',
clear: 'リセット'
},
LegacyTransfer: {
sourceTitle: '元',
targetTitle: '先'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'データなし'

View File

@ -57,10 +57,18 @@ const koKR: NLocale = {
confirm: '확인',
clear: '지우기'
},
Transfer: {
LegacyTransfer: {
sourceTitle: '원본',
targetTitle: '타깃'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: '데이터 없음'
},

View File

@ -58,9 +58,17 @@ const nbNO: NLocale = {
confirm: 'Bekreft',
clear: 'Tøm'
},
LegacyTransfer: {
sourceTitle: 'Kilde',
targetTitle: 'Mål'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Ingen data'

View File

@ -56,10 +56,18 @@ const nlNL: NLocale = {
confirm: 'Bevestig',
clear: 'Wis'
},
Transfer: {
LegacyTransfer: {
sourceTitle: 'Bron',
targetTitle: 'Doel'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Geen Data'
},

View File

@ -57,9 +57,17 @@ const plPL: NLocale = {
confirm: 'Potwierdź',
clear: 'Wyczyść'
},
LegacyTransfer: {
sourceTitle: 'Źródło',
targetTitle: 'Cel'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Brak danych'

View File

@ -57,10 +57,18 @@ const ptBR: NLocale = {
confirm: 'Confirmar',
clear: 'Limpar'
},
Transfer: {
LegacyTransfer: {
sourceTitle: 'Fonte',
targetTitle: 'Destino'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Não há dados'
},

View File

@ -58,9 +58,17 @@ const ruRu: NLocale = {
confirm: 'Подтвердить',
clear: 'Очистить'
},
LegacyTransfer: {
sourceTitle: 'Источник',
targetTitle: 'Назначение'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Нет данных'

View File

@ -57,9 +57,17 @@ const skSK: NLocale = {
confirm: 'Potvrdiť',
clear: 'Vyčistiť'
},
LegacyTransfer: {
sourceTitle: 'Zdroj',
targetTitle: 'Cieľ'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Žiadne dáta'

View File

@ -57,10 +57,18 @@ const thTH: NLocale = {
confirm: 'ยืนยัน',
clear: 'ล้างข้อมูล'
},
Transfer: {
LegacyTransfer: {
sourceTitle: 'Source',
targetTitle: 'Target'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'ไม่มีข้อมูล'
},

View File

@ -56,9 +56,17 @@ const ukUA: NLocale = {
confirm: 'Підтвердити',
clear: 'Стерти'
},
LegacyTransfer: {
sourceTitle: 'Джерело',
targetTitle: 'Ціль'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selectedTotal: (num: number): string => `Selected ${num} items`
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Немає даних'

View File

@ -56,10 +56,18 @@ const viVN: NLocale = {
confirm: 'Xác nhận',
clear: 'Xóa'
},
Transfer: {
LegacyTransfer: {
sourceTitle: 'Nguồn',
targetTitle: 'Đích'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selected: (num: number): string => `${num} items selected`
},
Empty: {
description: 'Không có dữ liệu'
},

View File

@ -56,9 +56,16 @@ const zhCN: NLocale = {
confirm: '确认',
clear: '重置'
},
LegacyTransfer: {
sourceTitle: '源项',
targetTitle: '目标项'
},
Transfer: {
selectAll: '全选',
clearAll: '清除',
unselectAll: '取消全选',
total: (num: number): string => `${num}`,
selectedTotal: (num: number): string => `已选 ${num}`
selected: (num: number): string => `已选 ${num}`
},
Empty: {
description: '无数据'

View File

@ -56,9 +56,17 @@ const zhTW: NLocale = {
confirm: '確認',
clear: '重置'
},
LegacyTransfer: {
sourceTitle: '源項',
targetTitle: '目標項'
},
Transfer: {
// TODO: translation
selectAll: '全选',
unselectAll: '取消全选',
clearAll: '清除',
total: (num: number): string => `${num}`,
selectedTotal: (num: number): string => `已選 ${num}`
selected: (num: number): string => `已選 ${num}`
},
Empty: {
description: '無數據'

View File

@ -38,6 +38,7 @@ import { imageDark } from '../image/styles'
import { inputDark } from '../input/styles'
import { inputNumberDark } from '../input-number/styles'
import { layoutDark } from '../layout/styles'
import { legacyTransferDark } from '../legacy-transfer/styles'
import { listDark } from '../list/styles'
import { loadingBarDark } from '../loading-bar/styles'
import { logDark } from '../log/styles'
@ -120,6 +121,7 @@ export const darkTheme: BuiltInGlobalTheme = {
Image: imageDark,
Input: inputDark,
InputNumber: inputNumberDark,
LegacyTransfer: legacyTransferDark,
Layout: layoutDark,
List: listDark,
LoadingBar: loadingBarDark,

View File

@ -40,6 +40,7 @@ import { imageLight } from '../image/styles'
import { inputLight } from '../input/styles'
import { inputNumberLight } from '../input-number/styles'
import { layoutLight } from '../layout/styles'
import { legacyTransferLight } from '../legacy-transfer/styles'
import { listLight } from '../list/styles'
import { loadingBarLight } from '../loading-bar/styles'
import { logLight } from '../log/styles'
@ -123,6 +124,7 @@ export const lightTheme: BuiltInGlobalTheme = {
Input: inputLight,
InputNumber: inputNumberLight,
Layout: layoutLight,
LegacyTransfer: legacyTransferLight,
List: listLight,
LoadingBar: loadingBarLight,
Log: logLight,

View File

@ -1,7 +1,7 @@
<markdown>
# Basic
Basic example of the Transfer component. If you have tons of data, see below for virtualised lists.
Basic example of the Transfer component. If you have tons of data, see below for virtualized list.
</markdown>
<template>
@ -19,15 +19,11 @@ function createOptions () {
}))
}
function createValues () {
return Array.from({ length: 50 }).map((v, i) => i)
}
export default defineComponent({
setup () {
return {
options: createOptions(),
value: ref(createValues())
value: ref([])
}
}
})

View File

@ -1,43 +1,41 @@
# Transfer
<!--single-column-->
A more efficient transfer.
~~Left, right, right, left... I'm a simple man, and I can play this all day.~~
Now, the transfer's style is simple and efficient. I can't continue to play.
If you want to use original transfer, please refer to [Legacy Transfer](legacy-transfer). Please note that the legacy transfer will be removed in the next major version. It's not recommended to to use it.
## Demos
```demo
basic.vue
large-data.vue
size.vue
filterable.vue
render-label
render-source-list
render-label.vue
render-source-list.vue
```
## API
### Transfer Props
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| default-value | `Array<string \| number> \| null` | `null` | Default value. |
| disabled | `boolean` | `true` | Disabled state. |
| filterable | `boolean` | `false` | Filterable state. |
| filter | `function` | `(pattern: string, option: TransferOption, from: 'source' \| 'target') => boolean` | A basic label string match function. |
| options | `Array<TransferOption>` | `[]` | For configuration options, see the TransferOption Type below. |
| render-label | `({ from, option }: { from: 'source' \| 'target', option: TransferOption }) => VNodeChild` | `undefined` | Customize label rendering. |
| render-source-list | `({ onCheck, checkedOptions, pattern }: { onCheck: (checkedValueList: Array<OptionValue>) => void, checkedOptions: Array<Option>, pattern: string }) => VNodeChild` | `undefined` | Customize source list rendering. |
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | Size. |
| source-filter-placeholder | `string` | `undefined` | Placeholder for the source items search box. |
| source-title | `string` | `'Source'` | Source items title. |
| target-filter-placeholder | `string` | `undefined` | Placeholder for the target items search box. |
| target-title | `string` | `'Target'` | Target items title. |
| value | `Array<string \| number> \| null` | `undefined` | Value when being set manually. |
| on-update:value | `(value: Array<string \| number>) => void` | `undefined` | Callback when the value changes. |
| virtual-scroll | `boolean` | `false` | Enable virtual scrolling. |
| Name | Type | Default | Description | Version |
| --- | --- | --- | --- | --- |
| default-value | `Array<string \| number> \| null` | `null` | Default value. | |
| disabled | `boolean` | `true` | Disabled state. | |
| filterable | `boolean` | `false` | Filterable state. | |
| filter | `(pattern: string, option: TransferOption) => boolean` | A basic label string match function. | |
| options | `TransferOption[]` | `[]` | For configuration options, see the TransferOption Type below. | |
| render-source-label | `(props: { option: TransferOption }) => VNodeChild` | `undefined` | Customize source label rendering. | NEXT_VERSION |
| render-target-label | `(props: { option: TransferOption }) => VNodeChild` | `undefined` | Customize target label rendering. | NEXT_VERSION |
| render-source-list | `(props: { onCheck: (checkedValueList: Array<string \| number>) => void, checkedOptions: TransferOption[], pattern: string }) => VNodeChild` | `undefined` | Customize source list rendering. | NEXT_VERSION |
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | Size. | |
| source-filter-placeholder | `string` | `undefined` | Placeholder for the source items search box. | |
| source-title | `string` | `'Source'` | Source items title. | |
| target-filter-placeholder | `string` | `undefined` | Placeholder for the target items search box. | |
| target-title | `string` | `'Target'` | Target items title. | |
| value | `Array<string \| number> \| null` | `undefined` | Value when being set manually. | |
| on-update:value | `(value: Array<string \| number>) => void` | `undefined` | Callback when the value changes. | |
| virtual-scroll | `boolean` | `false` | Enable virtual scrolling. | |
#### TransferOption Type

View File

@ -1,9 +1,5 @@
<markdown>
# Large Data
~~If you have tons of data, you may need to speed the transfer up! Set virtual-scroll on transfer to use a blazing fast transfer (which turns the ridiculous animation off).~~
Now we don't have to worry about the grinding animation.
# Large data
</markdown>
<template>

View File

@ -1,84 +0,0 @@
# Customize label rendering
It can be changed into address book, menu, etc. there are many application scenarios.
```html
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
:renderLabel="renderLabel"
/>
```
```js
import { defineComponent, ref, h } from 'vue'
import { NAvatar } from 'naive-ui'
const options = [
{
label: '07akioni',
value: 'https://avatars.githubusercontent.com/u/18677354?s=60&v=4'
},
{
label: 'amadeus711',
value: 'https://avatars.githubusercontent.com/u/46394163?s=60&v=4'
},
{
label: 'Talljack',
value: 'https://avatars.githubusercontent.com/u/34439652?s=60&v=4'
},
{
label: 'JiwenBai',
value: 'https://avatars.githubusercontent.com/u/43430022?s=60&v=4'
},
{
label: 'songjianet',
value: 'https://avatars.githubusercontent.com/u/19239641?s=60&v=4'
}
]
export default defineComponent({
setup () {
return {
options,
value: ref([options[0].value]),
renderLabel: function ({ from, option }) {
return h(
'div',
{
style: {
display: 'flex',
margin: from === 'source' ? undefined : '5px 0'
}
},
{
default: () => [
from === 'source'
? undefined
: h(NAvatar, {
round: true,
src: option.value,
size: 'small',
fallbackSrc:
'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg'
}),
h(
'div',
{
style: {
display: 'flex',
marginLeft: from === 'source' ? undefined : '5px',
alignSelf: 'center'
}
},
{ default: () => option.label }
)
]
}
)
}
}
}
})
```

View File

@ -0,0 +1,85 @@
<markdown>
# Custom label
Transfer can be applied for many scenarios.
</markdown>
<template>
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
:render-target-label="renderLabel"
/>
</template>
<script lang="ts">
import { defineComponent, ref, h } from 'vue'
import { NAvatar, TransferRenderTargetLabel } from 'naive-ui'
const options = [
{
label: '07akioni',
value: 'https://avatars.githubusercontent.com/u/18677354?s=60&v=4'
},
{
label: 'amadeus711',
value: 'https://avatars.githubusercontent.com/u/46394163?s=60&v=4'
},
{
label: 'Talljack',
value: 'https://avatars.githubusercontent.com/u/34439652?s=60&v=4'
},
{
label: 'JiwenBai',
value: 'https://avatars.githubusercontent.com/u/43430022?s=60&v=4'
},
{
label: 'songjianet',
value: 'https://avatars.githubusercontent.com/u/19239641?s=60&v=4'
}
]
export default defineComponent({
setup () {
const renderLabel: TransferRenderTargetLabel = function ({ option }) {
return h(
'div',
{
style: {
display: 'flex',
margin: '6px 0'
}
},
{
default: () => [
h(NAvatar, {
round: true,
src: option.value as string,
size: 'small',
fallbackSrc:
'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg'
}),
h(
'div',
{
style: {
display: 'flex',
marginLeft: '6px',
alignSelf: 'center'
}
},
{ default: () => option.label }
)
]
}
)
}
return {
options,
value: ref([options[0].value]),
renderLabel
}
}
})
</script>

View File

@ -1,70 +0,0 @@
# Customize source list rendering
```html
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
:renderSourceList="renderSourceList"
filterable
/>
```
```js
import { defineComponent, ref, h } from 'vue'
import { repeat } from 'seemly'
import { NTree } from 'naive-ui'
function createLabel (level) {
if (level === 4) return 'Out of Tao, One is born'
if (level === 3) return 'Out of One, Two'
if (level === 2) return 'Out of Two, Three'
if (level === 1) return 'Out of Three, the created universe'
return ''
}
function createData (level = 4, baseKey = '') {
if (!level) return undefined
return repeat(6 - level, undefined).map((_, index) => {
const value = '' + baseKey + level + index
return {
label: createLabel(level),
value,
children: createData(level - 1, value)
}
})
}
function flattenTree (list) {
const result = []
function flatten (_list = []) {
_list.forEach((item) => {
result.push(item)
flatten(item.children)
})
}
flatten(list)
return result
}
export default defineComponent({
setup () {
return {
options: flattenTree(createData()),
value: ref([]),
renderSourceList: function ({ onCheck, checkedOptions, pattern }) {
return h(NTree, {
keyField: 'value',
checkable: true,
data: createData(),
pattern,
checkedKeys: checkedOptions.map((i) => i.value),
onUpdateCheckedKeys: function (_, option) {
onCheck(option.map((i) => i.value))
}
})
}
}
}
})
```

View File

@ -0,0 +1,84 @@
<markdown>
# Custom source list
</markdown>
<template>
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
:render-source-list="renderSourceList"
filterable
/>
</template>
<script lang="ts">
import { defineComponent, ref, h } from 'vue'
import { repeat } from 'seemly'
import { NTree, TransferRenderSourceList } from 'naive-ui'
function createLabel (level: number): string {
if (level === 4) return 'Foo'
if (level === 3) return 'Bar'
if (level === 2) return 'Baz'
if (level === 1) return '???'
return ''
}
type Option = {
label: string
value: string
children?: Option[]
}
function createData (level = 4, baseKey = ''): Option[] | undefined {
if (!level) return undefined
return repeat(6 - level, undefined).map((_, index) => {
const value = '' + baseKey + level + index
return {
label: createLabel(level),
value,
children: createData(level - 1, value)
}
})
}
function flattenTree (list: undefined | Option[]): Option[] {
const result: Option[] = []
function flatten (_list: Option[] = []) {
_list.forEach((item) => {
result.push(item)
flatten(item.children)
})
}
flatten(list)
return result
}
export default defineComponent({
setup () {
const treeData = createData()
const valueRef = ref<Array<string | number>>([])
const renderSourceList: TransferRenderSourceList = function ({
onCheck,
pattern
}) {
return h(NTree, {
keyField: 'value',
checkable: true,
data: treeData,
pattern,
checkedKeys: valueRef.value,
onUpdateCheckedKeys: (checkedKeys: Array<string | number>) => {
onCheck(checkedKeys)
}
})
}
return {
options: flattenTree(createData()),
value: valueRef,
renderSourceList
}
}
})
</script>

View File

@ -1,43 +1,41 @@
# 穿梭框 Transfer
<!--single-column-->
一个更高效穿梭框。
~~左、右、左、右...像我这么无聊的人能玩一整天。~~
现在的样式简洁高效,没得玩了。
如果你需要使用原有的穿梭框,请参考 [旧版穿梭框](legacy-transfer),需要注意旧版的穿梭框会在下一个主版本被移除,不建议使用。
## 演示
```demo
basic.vue
large-data.vue
size.vue
filterable.vue
render-label
render-source-list
render-label.vue
render-source-list.vue
```
## API
### Transfer Props
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| default-value | `Array<string \| number> \| null` | `null` | 非受控模式下的默认值 |
| disabled | `boolean` | `true` | 是否禁用 |
| filterable | `boolean` | `false` | 是否可过滤 |
| filter | `(pattern: string, option: TransferOption, from: 'source' \| 'target') => boolean` | 一个简单的标签字符串匹配函数 | 搜索时使用的过滤函数 |
| options | `Array<TransferOption>` | `[]` | 配置选项内容,详情见 TransferOption Type |
| render-label | `({ from, option }: { from: 'source' \| 'target', option: TransferOption }) => VNodeChild` | `undefined` | 自定义标签 |
| render-source-list | `({ onCheck, checkedOptions, pattern }: { onCheck: (checkedValueList: Array<OptionValue>) => void, checkedOptions: Array<Option>, pattern: string }) => VNodeChild` | `undefined` | 自定义源列表 |
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | 尺寸 |
| source-filter-placeholder | `string` | `undefined` | 源项搜索框中的占位符 |
| source-title | `string` | `'源项'` | 源项标题 |
| target-filter-placeholder | `string` | `undefined` | 目标项搜索框中的占位符 |
| target-title | `string` | `'目标项'` | 目标项标题 |
| value | `Array<string \| number> \| null` | `undefined` | 受控模式下的值 |
| on-update:value | `(value: Array<string \| number>) => void` | `undefined` | 值发生改变时的回调 |
| virtual-scroll | `boolean` | `false` | 是否启用虚拟滚动 |
| 名称 | 类型 | 默认值 | 说明 | 版本 |
| --- | --- | --- | --- | --- |
| default-value | `Array<string \| number> \| null` | `null` | 非受控模式下的默认值 | |
| disabled | `boolean` | `true` | 是否禁用 | |
| filterable | `boolean` | `false` | 是否可过滤 | |
| filter | `(pattern: string, option: TransferOption) => boolean` | 一个简单的标签字符串匹配函数 | 搜索时使用的过滤函数 | |
| options | `TransferOption[]` | `[]` | 配置选项内容,详情见 TransferOption Type | |
| render-source-label | `(props: { from: 'source' \| 'target', option: TransferOption }) => VNodeChild` | `undefined` | 自定义源标签 | NEXT_VERSION |
| render-target-label | `(props: { from: 'source' \| 'target', option: TransferOption }) => VNodeChild` | `undefined` | 自定义目标标签 | NEXT_VERSION |
| render-source-list | `(props: { onCheck: (checkedValueList: Array<string \| number>) => void, checkedOptions: TransferOption[], pattern: string }) => VNodeChild` | `undefined` | 自定义源列表 | NEXT_VERSION |
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | 尺寸 | |
| source-filter-placeholder | `string` | `undefined` | 源项搜索框中的占位符 | |
| source-title | `string` | `'源项'` | 源项标题 | |
| target-filter-placeholder | `string` | `undefined` | 目标项搜索框中的占位符 | |
| target-title | `string` | `'目标项'` | 目标项标题 | |
| value | `Array<string \| number> \| null` | `undefined` | 受控模式下的值 | |
| on-update:value | `(value: Array<string \| number>) => void` | `undefined` | 值发生改变时的回调 | |
| virtual-scroll | `boolean` | `false` | 是否启用虚拟滚动 | |
#### TransferOption Type

View File

@ -1,9 +1,5 @@
<markdown>
# 一大堆数据
~~如果你有一大堆数据你可能想让它快一点设定 virtual-scroll 来使用一个飞快的穿梭框会关掉那个傻乎乎的动画~~
现在不用操心那磨人的动画了
</markdown>
<template>

View File

@ -1,84 +0,0 @@
# 自定义标签
可以变成通讯录、菜单等,应用场景挺多
```html
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
:renderLabel="renderLabel"
/>
```
```js
import { defineComponent, ref, h } from 'vue'
import { NAvatar } from 'naive-ui'
const options = [
{
label: '07akioni',
value: 'https://avatars.githubusercontent.com/u/18677354?s=60&v=4'
},
{
label: 'amadeus711',
value: 'https://avatars.githubusercontent.com/u/46394163?s=60&v=4'
},
{
label: 'Talljack',
value: 'https://avatars.githubusercontent.com/u/34439652?s=60&v=4'
},
{
label: 'JiwenBai',
value: 'https://avatars.githubusercontent.com/u/43430022?s=60&v=4'
},
{
label: 'songjianet',
value: 'https://avatars.githubusercontent.com/u/19239641?s=60&v=4'
}
]
export default defineComponent({
setup () {
return {
options,
value: ref([options[0].value]),
renderLabel: function ({ from, option }) {
return h(
'div',
{
style: {
display: 'flex',
margin: from === 'source' ? undefined : '5px 0'
}
},
{
default: () => [
from === 'source'
? undefined
: h(NAvatar, {
round: true,
src: option.value,
size: 'small',
fallbackSrc:
'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg'
}),
h(
'div',
{
style: {
display: 'flex',
marginLeft: from === 'source' ? undefined : '5px',
alignSelf: 'center'
}
},
{ default: () => option.label }
)
]
}
)
}
}
}
})
```

View File

@ -0,0 +1,85 @@
<markdown>
# 自定义标签
可以变成通讯录菜单等应用场景挺多
</markdown>
<template>
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
:render-target-label="renderLabel"
/>
</template>
<script lang="ts">
import { defineComponent, ref, h } from 'vue'
import { NAvatar, TransferRenderTargetLabel } from 'naive-ui'
const options = [
{
label: '07akioni',
value: 'https://avatars.githubusercontent.com/u/18677354?s=60&v=4'
},
{
label: 'amadeus711',
value: 'https://avatars.githubusercontent.com/u/46394163?s=60&v=4'
},
{
label: 'Talljack',
value: 'https://avatars.githubusercontent.com/u/34439652?s=60&v=4'
},
{
label: 'JiwenBai',
value: 'https://avatars.githubusercontent.com/u/43430022?s=60&v=4'
},
{
label: 'songjianet',
value: 'https://avatars.githubusercontent.com/u/19239641?s=60&v=4'
}
]
export default defineComponent({
setup () {
const renderLabel: TransferRenderTargetLabel = function ({ option }) {
return h(
'div',
{
style: {
display: 'flex',
margin: '6px 0'
}
},
{
default: () => [
h(NAvatar, {
round: true,
src: option.value as string,
size: 'small',
fallbackSrc:
'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg'
}),
h(
'div',
{
style: {
display: 'flex',
marginLeft: '6px',
alignSelf: 'center'
}
},
{ default: () => option.label }
)
]
}
)
}
return {
options,
value: ref([options[0].value]),
renderLabel
}
}
})
</script>

View File

@ -1,70 +0,0 @@
# 自定义列表
```html
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
:renderSourceList="renderSourceList"
filterable
/>
```
```js
import { defineComponent, ref, h } from 'vue'
import { repeat } from 'seemly'
import { NTree } from 'naive-ui'
function createLabel (level) {
if (level === 4) return '道生一'
if (level === 3) return '一生二'
if (level === 2) return '二生三'
if (level === 1) return '三生万物'
return ''
}
function createData (level = 4, baseKey = '') {
if (!level) return undefined
return repeat(6 - level, undefined).map((_, index) => {
const value = '' + baseKey + level + index
return {
label: createLabel(level),
value,
children: createData(level - 1, value)
}
})
}
function flattenTree (list) {
const result = []
function flatten (_list = []) {
_list.forEach((item) => {
result.push(item)
flatten(item.children)
})
}
flatten(list)
return result
}
export default defineComponent({
setup () {
return {
options: flattenTree(createData()),
value: ref([]),
renderSourceList: function ({ onCheck, checkedOptions, pattern }) {
return h(NTree, {
keyField: 'value',
checkable: true,
data: createData(),
pattern,
checkedKeys: checkedOptions.map((i) => i.value),
onUpdateCheckedKeys: function (_, option) {
onCheck(option.map((i) => i.value))
}
})
}
}
}
})
```

View File

@ -0,0 +1,84 @@
<markdown>
# 自定义列表
</markdown>
<template>
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
:render-source-list="renderSourceList"
filterable
/>
</template>
<script lang="ts">
import { defineComponent, ref, h } from 'vue'
import { repeat } from 'seemly'
import { NTree, TransferRenderSourceList } from 'naive-ui'
function createLabel (level: number): string {
if (level === 4) return '道生一'
if (level === 3) return '一生二'
if (level === 2) return '二生三'
if (level === 1) return '三生万物'
return ''
}
type Option = {
label: string
value: string
children?: Option[]
}
function createData (level = 4, baseKey = ''): Option[] | undefined {
if (!level) return undefined
return repeat(6 - level, undefined).map((_, index) => {
const value = '' + baseKey + level + index
return {
label: createLabel(level),
value,
children: createData(level - 1, value)
}
})
}
function flattenTree (list: undefined | Option[]): Option[] {
const result: Option[] = []
function flatten (_list: Option[] = []) {
_list.forEach((item) => {
result.push(item)
flatten(item.children)
})
}
flatten(list)
return result
}
export default defineComponent({
setup () {
const treeData = createData()
const valueRef = ref<Array<string | number>>([])
const renderSourceList: TransferRenderSourceList = function ({
onCheck,
pattern
}) {
return h(NTree, {
keyField: 'value',
checkable: true,
data: treeData,
pattern,
checkedKeys: valueRef.value,
onUpdateCheckedKeys: (checkedKeys: Array<string | number>) => {
onCheck(checkedKeys)
}
})
}
return {
options: flattenTree(createData()),
value: valueRef,
renderSourceList
}
}
})
</script>

View File

@ -1,3 +1,8 @@
export { default as NTransfer, transferProps } from './src/Transfer'
export type { TransferProps } from './src/Transfer'
export type { Option as TransferOption } from './src/interface'
export type {
Option as TransferOption,
TransferRenderSourceLabel,
TransferRenderTargetLabel,
RenderSourceListType as TransferRenderSourceList
} from './src/interface'

View File

@ -4,14 +4,17 @@ import {
h,
provide,
PropType,
CSSProperties
CSSProperties,
watchEffect,
toRef
} from 'vue'
import { useIsMounted } from 'vooks'
import { depx } from 'seemly'
import { NScrollbar } from '../../_internal'
import { useFormItem, useTheme, useConfig } from '../../_mixins'
import type { ThemeProps } from '../../_mixins'
import { createKey } from '../../_utils/cssr'
import { warn, call, ExtractPublicPropTypes } from '../../_utils'
import { call, ExtractPublicPropTypes, warnOnce } from '../../_utils'
import type { MaybeArray } from '../../_utils'
import { transferLight } from '../styles'
import type { TransferTheme } from '../styles'
@ -19,17 +22,17 @@ import NTransferHeader from './TransferHeader'
import NTransferList from './TransferList'
import NTransferFilter from './TransferFilter'
import { useTransferData } from './use-transfer-data'
import style from './styles/index.cssr'
import {
OptionValue,
Option,
Filter,
OnUpdateValue,
transferInjectionKey,
RenderLabelType,
RenderSourceListType
TransferRenderTargetLabel,
RenderSourceListType,
TransferRenderSourceLabel
} from './interface'
import { NScrollbar } from '../../_internal'
import style from './styles/index.cssr'
export const transferProps = {
...(useTheme.props as ThemeProps<TransferTheme>),
@ -62,23 +65,12 @@ export const transferProps = {
}
},
size: String as PropType<'small' | 'medium' | 'large'>,
renderLabel: Function as PropType<RenderLabelType>,
renderSourceLabel: Function as PropType<TransferRenderSourceLabel>,
renderTargetLabel: Function as PropType<TransferRenderTargetLabel>,
renderSourceList: Function as PropType<RenderSourceListType>,
'onUpdate:value': [Function, Array] as PropType<MaybeArray<OnUpdateValue>>,
onUpdateValue: [Function, Array] as PropType<MaybeArray<OnUpdateValue>>,
onChange: {
type: [Function, Array] as PropType<MaybeArray<OnUpdateValue>>,
validator: () => {
if (__DEV__) {
warn(
'transfer',
'`on-change` is deprecated, please use `on-update:value` instead.'
)
}
return true
},
default: undefined
}
onChange: [Function, Array] as PropType<MaybeArray<OnUpdateValue>>
} as const
export type TransferProps = ExtractPublicPropTypes<typeof transferProps>
@ -87,6 +79,16 @@ export default defineComponent({
name: 'Transfer',
props: transferProps,
setup (props) {
if (__DEV__) {
watchEffect(() => {
if (props.onChange !== undefined) {
warnOnce(
'transfer',
'`on-change` is deprecated, please use `on-update:value` instead.'
)
}
})
}
const { mergedClsPrefixRef } = useConfig(props)
const themeRef = useTheme(
'Transfer',
@ -106,18 +108,17 @@ export default defineComponent({
return depx(itemSize)
})
const {
uncontrolledValue: uncontrolledValueRef,
mergedValue: mergedValueRef,
tgtValueSet: tgtValueSetRef,
avlSrcValueSet: avlSrcValueSetRef,
tgtOpts: tgtOptsRef,
srcOpts: srcOptsRef,
filteredSrcOpts: filteredSrcOptsRef,
headerBtnStatus: headerBtnStatusRef,
srcPattern: srcPatternRef,
isInputing: isInputingRef,
handleInputFocus,
handleInputBlur,
uncontrolledValueRef,
mergedValueRef,
targetValueSetRef,
valueSetForSelectAllRef,
valueSetForUnselectAllRef,
targetOptionsRef,
filteredSrcOptionsRef,
canNotSelectAnythingRef,
canBeClearedRef,
allCheckedRef,
srcPatternRef,
handleSrcFilterUpdateValue
} = useTransferData(props)
function doUpdateValue (value: OptionValue[]): void {
@ -136,11 +137,11 @@ export default defineComponent({
}
function handleClearAll (): void {
doUpdateValue([])
doUpdateValue([...valueSetForUnselectAllRef.value])
}
function handleCheckedAll (): void {
doUpdateValue([...avlSrcValueSetRef.value])
doUpdateValue([...valueSetForSelectAllRef.value])
}
function handleItemCheck (checked: boolean, optionValue: OptionValue): void {
@ -161,28 +162,29 @@ export default defineComponent({
}
provide(transferInjectionKey, {
tgtValueSetRef,
targetValueSetRef,
mergedClsPrefixRef,
disabledRef: mergedDisabledRef,
mergedThemeRef: themeRef,
srcOptsRef,
tgtOptsRef,
headerBtnStatusRef,
targetOptionsRef,
canNotSelectAnythingRef,
canBeClearedRef,
allCheckedRef,
srcOptionsLengthRef: computed(() => props.options.length),
handleItemCheck,
renderLabel: props.renderLabel
renderSourceLabelRef: toRef(props, 'renderSourceLabel'),
renderTargetLabelRef: toRef(props, 'renderTargetLabel')
})
return {
mergedClsPrefix: mergedClsPrefixRef,
mergedDisabled: mergedDisabledRef,
itemSize: itemSizeRef,
isMounted: useIsMounted(),
isInputing: isInputingRef,
mergedTheme: themeRef,
filteredSrcOpts: filteredSrcOptsRef,
tgtOpts: tgtOptsRef,
filteredSrcOpts: filteredSrcOptionsRef,
tgtOpts: targetOptionsRef,
srcPattern: srcPatternRef,
handleInputFocus,
handleInputBlur,
mergedSize: mergedSizeRef,
handleSrcFilterUpdateValue,
handleCheckedAll,
handleClearAll,
@ -193,7 +195,6 @@ export default defineComponent({
const {
common: { cubicBezierEaseInOut },
self: {
width,
borderRadius,
borderColor,
listColor,
@ -203,12 +204,21 @@ export default defineComponent({
itemTextColor,
itemColorPending,
itemTextColorDisabled,
extraFontSize,
titleFontWeight,
iconColor,
iconColorDisabled,
closeColorHover,
closeColorPressed,
closeIconColor,
closeIconColorHover,
closeIconColorPressed,
closeIconSize,
closeSize,
dividerColor,
extraTextColorDisabled,
[createKey('extraFontSize', size)]: extraFontSize,
[createKey('fontSize', size)]: fontSize,
[createKey('itemHeight', size)]: itemHeight
[createKey('titleFontSize', size)]: titleFontSize,
[createKey('itemHeight', size)]: itemHeight,
[createKey('headerHeight', size)]: headerHeight
}
} = themeRef.value
return {
@ -217,7 +227,9 @@ export default defineComponent({
'--n-border-radius': borderRadius,
'--n-extra-font-size': extraFontSize,
'--n-font-size': fontSize,
'--n-header-font-size': titleFontSize,
'--n-header-extra-text-color': extraTextColor,
'--n-header-extra-text-color-disabled': extraTextColorDisabled,
'--n-header-font-weight': titleFontWeight,
'--n-header-text-color': titleTextColor,
'--n-header-text-color-disabled': titleTextColorDisabled,
@ -226,9 +238,15 @@ export default defineComponent({
'--n-item-text-color': itemTextColor,
'--n-item-text-color-disabled': itemTextColorDisabled,
'--n-list-color': listColor,
'--n-width': width,
'--n-icon-color': iconColor,
'--n-icon-color-disabled': iconColorDisabled
'--n-header-height': headerHeight,
'--n-close-size': closeSize,
'--n-close-icon-size': closeIconSize,
'--n-close-color-hover': closeColorHover,
'--n-close-color-pressed': closeColorPressed,
'--n-close-icon-color': closeIconColor,
'--n-close-icon-color-hover': closeIconColorHover,
'--n-close-icon-color-pressed': closeIconColorPressed,
'--n-divider-color': dividerColor
}
})
}
@ -244,12 +262,15 @@ export default defineComponent({
]}
style={this.cssVars as CSSProperties}
>
<div class={`${mergedClsPrefix}-transfer-list`}>
<div
class={`${mergedClsPrefix}-transfer-list ${mergedClsPrefix}-transfer-list--source`}
>
<NTransferHeader
source
title={this.sourceTitle}
onCheckedAll={this.handleCheckedAll}
onClearAll={this.handleClearAll}
title={this.sourceTitle}
size={this.mergedSize}
/>
<div class={`${mergedClsPrefix}-transfer-list-body`}>
{this.filterable ? (
@ -258,8 +279,6 @@ export default defineComponent({
value={this.srcPattern}
disabled={this.mergedDisabled}
placeholder={this.sourceFilterPlaceholder}
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}
/>
) : null}
<div class={`${mergedClsPrefix}-transfer-list-flex-container`}>
@ -283,8 +302,6 @@ export default defineComponent({
options={this.filteredSrcOpts}
disabled={this.mergedDisabled}
virtualScroll={this.virtualScroll}
isMounted={this.isMounted}
isInputing={this.isInputing}
itemSize={this.itemSize}
/>
)}
@ -292,10 +309,13 @@ export default defineComponent({
</div>
<div class={`${mergedClsPrefix}-transfer-list__border`} />
</div>
<div class={`${mergedClsPrefix}-transfer-list`}>
<div
class={`${mergedClsPrefix}-transfer-list ${mergedClsPrefix}-transfer-list--target`}
>
<NTransferHeader
title={this.targetTitle}
onClearAll={this.handleClearAll}
size={this.mergedSize}
title={this.targetTitle}
/>
<div class={`${mergedClsPrefix}-transfer-list-body`}>
<div class={`${mergedClsPrefix}-transfer-list-flex-container`}>
@ -303,8 +323,6 @@ export default defineComponent({
options={this.tgtOpts}
disabled={this.mergedDisabled}
virtualScroll={this.virtualScroll}
isMounted={this.isMounted}
isInputing={this.isInputing}
itemSize={this.itemSize}
/>
</div>

View File

@ -10,14 +10,6 @@ export default defineComponent({
value: String,
placeholder: String,
disabled: Boolean,
onFocus: {
type: Function as PropType<() => void>,
required: true
},
onBlur: {
type: Function as PropType<() => void>,
required: true
},
onUpdateValue: {
type: Function as PropType<(value: string | null) => void>,
required: true
@ -39,20 +31,15 @@ export default defineComponent({
value={this.value}
onUpdateValue={this.onUpdateValue}
disabled={this.disabled}
placeholder={this.placeholder}
theme={mergedTheme.peers.Input}
themeOverrides={mergedTheme.peerOverrides.Input}
clearable
size="small"
placeholder={this.placeholder}
onFocus={this.onFocus}
onBlur={this.onBlur}
>
{{
'clear-icon-placeholder': () => (
<NBaseIcon
clsPrefix={mergedClsPrefix}
class={`${mergedClsPrefix}-transfer-icon`}
>
<NBaseIcon clsPrefix={mergedClsPrefix}>
{{ default: () => <SearchIcon /> }}
</NBaseIcon>
)

View File

@ -6,77 +6,77 @@ import { transferInjectionKey } from './interface'
export default defineComponent({
name: 'TransferHeader',
props: {
source: {
type: Boolean,
default: false
},
onCheckedAll: {
type: Function as PropType<() => void>
},
onClearAll: {
type: Function as PropType<() => void>
size: {
type: String as PropType<'small' | 'medium' | 'large'>,
required: true
},
source: Boolean,
onCheckedAll: Function as PropType<() => void>,
onClearAll: Function as PropType<() => void>,
title: String
},
setup (props) {
const {
srcOptsRef,
tgtOptsRef,
headerBtnStatusRef,
targetOptionsRef,
canNotSelectAnythingRef,
canBeClearedRef,
allCheckedRef,
mergedThemeRef,
disabledRef,
mergedClsPrefixRef
mergedClsPrefixRef,
srcOptionsLengthRef
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
} = inject(transferInjectionKey)!
const { localeRef } = useLocale('Transfer')
return () => {
const { source, onClearAll, onCheckedAll } = props
const { value: headerBtnStatus } = headerBtnStatusRef
const { value: mergedTheme } = mergedThemeRef
const { value: mergedClsPrefix } = mergedClsPrefixRef
const { value: locale } = localeRef
const buttonSize = props.size === 'large' ? 'small' : 'tiny'
const { title } = props
return (
<div class={`${mergedClsPrefix}-transfer-list-header`}>
{title && (
<div class={`${mergedClsPrefix}-transfer-list-header__title`}>
{title}
</div>
)}
{source && (
<div class={`${mergedClsPrefix}-transfer-list-header__button`}>
<NButton
theme={mergedTheme.peers.Button}
themeOverrides={mergedTheme.peerOverrides.Button}
size="tiny"
tertiary
onClick={headerBtnStatus.allChecked ? onClearAll : onCheckedAll}
disabled={headerBtnStatus.disabled || disabledRef.value}
>
{{
default: () =>
headerBtnStatus.allChecked ? '取消全选' : '全选'
}}
</NButton>
</div>
<NButton
class={`${mergedClsPrefix}-transfer-list-header__button`}
theme={mergedTheme.peers.Button}
themeOverrides={mergedTheme.peerOverrides.Button}
size={buttonSize}
tertiary
onClick={allCheckedRef.value ? onClearAll : onCheckedAll}
disabled={canNotSelectAnythingRef.value || disabledRef.value}
>
{{
default: () =>
allCheckedRef.value ? locale.unselectAll : locale.selectAll
}}
</NButton>
)}
{!source && headerBtnStatus.checked && (
<div class={`${mergedClsPrefix}-transfer-list-header__button`}>
<NButton
theme={mergedTheme.peers.Button}
themeOverrides={mergedTheme.peerOverrides.Button}
size="tiny"
tertiary
onClick={onClearAll}
disabled={headerBtnStatus.disabled || disabledRef.value}
>
{{
default: () => '清空'
}}
</NButton>
</div>
{!source && canBeClearedRef.value && (
<NButton
class={`${mergedClsPrefix}-transfer-list-header__button`}
theme={mergedTheme.peers.Button}
themeOverrides={mergedTheme.peerOverrides.Button}
size={buttonSize}
tertiary
onClick={onClearAll}
disabled={disabledRef.value}
>
{{
default: () => locale.clearAll
}}
</NButton>
)}
{/* <div class={`${mergedClsPrefix}-transfer-list-header__header`}>
{props.title}
</div> */}
<div class={`${mergedClsPrefix}-transfer-list-header__extra`}>
{source
? locale.total(srcOptsRef.value.length)
: locale.selectedTotal(tgtOptsRef.value.length)}
? locale.total(srcOptionsLengthRef.value)
: locale.selected(targetOptionsRef.value.length)}
</div>
</div>
)

View File

@ -1,13 +1,4 @@
import {
h,
defineComponent,
ref,
inject,
PropType,
TransitionGroup,
Transition,
Fragment
} from 'vue'
import { h, defineComponent, ref, inject, PropType } from 'vue'
import { VirtualList, VirtualListInst } from 'vueuc'
import { NEmpty } from '../../empty'
import { NScrollbar, ScrollbarInst } from '../../_internal'
@ -33,18 +24,7 @@ export default defineComponent({
type: Boolean,
required: true
},
isMounted: {
type: Boolean,
required: true
},
isInputing: {
type: Boolean,
required: true
},
source: {
type: Boolean,
default: false
}
source: Boolean
},
setup () {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@ -77,89 +57,71 @@ export default defineComponent({
}
},
render () {
const { mergedTheme, mergedClsPrefix, virtualScroll, syncVLScroller } = this
const { mergedTheme, options } = this
if (options.length === 0) {
return (
<NEmpty
theme={mergedTheme.peers.Empty}
themeOverrides={mergedTheme.peerOverrides.Empty}
/>
)
}
const { mergedClsPrefix, virtualScroll, source, disabled, syncVLScroller } =
this
return (
<>
<NScrollbar
ref="scrollerInstRef"
theme={mergedTheme.peers.Scrollbar}
themeOverrides={mergedTheme.peerOverrides.Scrollbar}
container={virtualScroll ? this.scrollContainer : undefined}
content={virtualScroll ? this.scrollContent : undefined}
>
{{
default: () =>
virtualScroll ? (
<VirtualList
ref="vlInstRef"
style={{ height: '100%' }}
class={`${mergedClsPrefix}-transfer-list-content`}
items={this.options}
itemSize={this.itemSize}
showScrollbar={false}
onResize={syncVLScroller}
onScroll={syncVLScroller}
keyField="value"
>
{{
default: ({ item }: { item: Option }) => {
const { source, disabled } = this
return (
<NTransferListItem
source={source}
key={item.value}
value={item.value}
disabled={item.disabled || disabled}
label={item.label}
option={item}
/>
)
}
}}
</VirtualList>
) : (
<div class={`${mergedClsPrefix}-transfer-list-content`}>
<TransitionGroup
name="item"
appear={this.isMounted}
css={!this.isInputing}
>
{{
default: () => {
const { source, disabled } = this
return this.options.map((option) => (
<NTransferListItem
source={source}
key={option.value}
value={option.value}
disabled={option.disabled || disabled}
label={option.label}
option={option}
/>
))
}
}}
</TransitionGroup>
</div>
)
}}
</NScrollbar>
<Transition
name="fade-in-transition"
appear={this.isMounted}
css={!this.isInputing}
>
{{
default: () =>
this.options.length ? null : (
<NEmpty
theme={mergedTheme.peers.Empty}
themeOverrides={mergedTheme.peerOverrides.Empty}
/>
)
}}
</Transition>
</>
<NScrollbar
ref="scrollerInstRef"
theme={mergedTheme.peers.Scrollbar}
themeOverrides={mergedTheme.peerOverrides.Scrollbar}
container={virtualScroll ? this.scrollContainer : undefined}
content={virtualScroll ? this.scrollContent : undefined}
>
{{
default: () =>
virtualScroll ? (
<VirtualList
ref="vlInstRef"
style={{ height: '100%' }}
class={`${mergedClsPrefix}-transfer-list-content`}
items={this.options}
itemSize={this.itemSize}
showScrollbar={false}
onResize={syncVLScroller}
onScroll={syncVLScroller}
keyField="value"
>
{{
default: ({ item }: { item: Option }) => {
const { source, disabled } = this
return (
<NTransferListItem
source={source}
key={item.value}
value={item.value}
disabled={item.disabled || disabled}
label={item.label}
option={item}
/>
)
}
}}
</VirtualList>
) : (
<div class={`${mergedClsPrefix}-transfer-list-content`}>
{this.options.map((option) => (
<NTransferListItem
source={source}
key={option.value}
value={option.value}
disabled={option.disabled || disabled}
label={option.label}
option={option}
/>
))}
</div>
)
}}
</NScrollbar>
)
}
})

View File

@ -1,17 +1,14 @@
import { h, inject, defineComponent, ref, PropType } from 'vue'
import { h, inject, defineComponent, PropType } from 'vue'
import { useMemo } from 'vooks'
import { NCheckbox } from '../../checkbox'
import { transferInjectionKey, Option } from './interface'
import { getTitleAttribute } from '../../_utils'
import { NBaseClose } from '../../_internal'
import { transferInjectionKey, Option } from './interface'
export default defineComponent({
name: 'NTransferListItem',
props: {
source: {
type: Boolean,
default: false
},
source: Boolean,
label: {
type: String,
required: true
@ -20,10 +17,7 @@ export default defineComponent({
type: [String, Number],
required: true
},
disabled: {
type: Boolean,
default: false
},
disabled: Boolean,
option: {
type: Object as PropType<Option>,
required: true
@ -31,35 +25,27 @@ export default defineComponent({
},
setup (props) {
const {
tgtValueSetRef,
targetValueSetRef,
mergedClsPrefixRef,
mergedThemeRef,
handleItemCheck,
renderLabel
renderSourceLabelRef,
renderTargetLabelRef
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
} = inject(transferInjectionKey)!
const checkedRef = useMemo(() => tgtValueSetRef.value.has(props.value))
const hasItemHoverRef = ref(false)
const checkedRef = useMemo(() => targetValueSetRef.value.has(props.value))
function handleClick (): void {
if (!props.disabled) {
handleItemCheck(!checkedRef.value, props.value)
}
}
function handleMouseEnter (): void {
hasItemHoverRef.value = true
}
function handleMouseLeave (): void {
hasItemHoverRef.value = false
}
return {
mergedClsPrefix: mergedClsPrefixRef,
mergedTheme: mergedThemeRef,
checked: checkedRef,
hasItemHover: hasItemHoverRef,
handleClick,
handleMouseEnter,
handleMouseLeave,
renderLabel
renderSourceLabel: renderSourceLabelRef,
renderTargetLabel: renderTargetLabelRef
}
},
render () {
@ -70,7 +56,8 @@ export default defineComponent({
label,
checked,
source,
renderLabel
renderSourceLabel,
renderTargetLabel
} = this
return (
<div
@ -81,10 +68,9 @@ export default defineComponent({
? `${mergedClsPrefix}-transfer-list-item--source`
: `${mergedClsPrefix}-transfer-list-item--target`
]}
onClick={() => (source ? this.handleClick() : null)}
onMouseenter={this.handleMouseEnter}
onMouseleave={this.handleMouseLeave}
onClick={source ? this.handleClick : undefined}
>
<div class={`${mergedClsPrefix}-transfer-list-item__background`} />
{source && (
<div class={`${mergedClsPrefix}-transfer-list-item__checkbox`}>
<NCheckbox
@ -99,20 +85,25 @@ export default defineComponent({
class={`${mergedClsPrefix}-transfer-list-item__label`}
title={getTitleAttribute(label)}
>
{renderLabel
? renderLabel({
from: source ? 'source' : 'target',
option: this.option
})
: label}
{source
? renderSourceLabel
? renderSourceLabel({
option: this.option
})
: label
: renderTargetLabel
? renderTargetLabel({
option: this.option
})
: label}
</div>
{!source && this.hasItemHover && !disabled && (
<div class={`${mergedClsPrefix}-transfer-list-item__close`}>
<NBaseClose
clsPrefix={mergedClsPrefix}
onClick={this.handleClick}
/>
</div>
{!source && !disabled && (
<NBaseClose
focusable={false}
class={`${mergedClsPrefix}-transfer-list-item__close`}
clsPrefix={mergedClsPrefix}
onClick={this.handleClick}
/>
)}
</div>
)

View File

@ -10,24 +10,14 @@ export interface Option {
disabled?: boolean
}
export interface CheckedStatus {
checked: boolean
allChecked: boolean
disabled: boolean
}
export type Filter = (
pattern: string,
option: Option,
from: 'source' | 'target'
) => boolean
export type Filter = (pattern: string, option: Option) => boolean
export interface RenderLabelProps {
from: 'source' | 'target'
option: Option
}
export type RenderLabelType = (props: RenderLabelProps) => VNodeChild
export type TransferRenderTargetLabel = (props: RenderLabelProps) => VNodeChild
export type TransferRenderSourceLabel = (props: RenderLabelProps) => VNodeChild
export interface RenderListProps {
onCheck: (checkedValueList: OptionValue[]) => void
@ -38,15 +28,18 @@ export interface RenderListProps {
export type RenderSourceListType = (props: RenderListProps) => VNodeChild
export interface TransferInjection {
tgtValueSetRef: Ref<Set<OptionValue>>
targetValueSetRef: Ref<Set<OptionValue>>
mergedClsPrefixRef: Ref<string>
disabledRef: Ref<boolean>
mergedThemeRef: Ref<MergedTheme<TransferTheme>>
srcOptsRef: Ref<Option[]>
tgtOptsRef: Ref<Option[]>
headerBtnStatusRef: Ref<CheckedStatus>
targetOptionsRef: Ref<Option[]>
canNotSelectAnythingRef: Ref<boolean>
canBeClearedRef: Ref<boolean>
allCheckedRef: Ref<boolean>
srcOptionsLengthRef: Ref<number>
handleItemCheck: (checked: boolean, value: OptionValue) => void
renderLabel: RenderLabelType | undefined
renderSourceLabelRef: Ref<TransferRenderSourceLabel | undefined>
renderTargetLabelRef: Ref<TransferRenderTargetLabel | undefined>
}
export const transferInjectionKey =

View File

@ -1,241 +1,199 @@
import { c, cB, cE, cM, cNotM } from '../../../_utils/cssr'
const animation = c([
c('@keyframes transfer-slide-in-from-left', `
0% {
transform: translateX(-150%);
}
100% {
transform: translateX(0);
}
`),
c('@keyframes transfer-slide-out-to-right', `
0% {
transform: translateX(0);
}
100% {
transform: translateX(150%);
}
`),
c('@keyframes transfer-slide-in-from-right', `
0% {
transform: translateX(150%);
}
100% {
transform: translateX(0);
}
`),
c('@keyframes transfer-slide-out-to-left', `
0% {
transform: translateX(0);
}
100% {
transform: translateX(-150%);
}
`),
c('@keyframes transfer-height-collapse', `
0% {
max-height: var(--n-item-height);
}
100% {
max-height: 0;
}
`),
c('@keyframes transfer-height-expand', `
0% {
max-height: 0;
}
100% {
max-height: var(--n-item-height);
}
`)
])
export default c([
cB('transfer', `
// --n-close-size
// --n-close-icon-size
// --n-close-color-hover
// --n-close-color-pressed
// --n-close-icon-color
// --n-close-icon-color-hover
// --n-close-icon-color-pressed
export default cB('transfer', `
width: 100%;
font-size: var(--n-font-size);
height: 300px;
display: flex;
flex-wrap: nowrap;
word-break: break-word;
`, [
cM('disabled', [
cB('transfer-list', [
cB('transfer-list-header', [
cE('title', `
color: var(--n-header-text-color-disabled);
`),
cE('extra', `
color: var(--n-header-extra-text-color-disabled);
`)
])
])
]),
cB('transfer-list', `
flex: 1;
min-width: 0;
height: inherit;
display: flex;
width: var(--n-width);
font-size: var(--n-font-size);
height: 240px;
display: flex;
flex-wrap: nowrap;
flex-direction: column;
background-clip: padding-box;
position: relative;
transition: background-color .3s var(--n-bezier);
background-color: var(--n-list-color);
`, [
cB('transfer-icon', `
color: var(--n-icon-color);
transition: color .3s var(--n-bezier);
`),
cM('disabled', [
cB('transfer-icon', {
color: 'var(--n-icon-color-disabled)'
})
cM('source', `
border-top-left-radius: var(--n-border-radius);
border-bottom-left-radius: var(--n-border-radius);
`, [
cE('border', 'border-right: 1px solid var(--n-divider-color);')
]),
cB('transfer-list', `
height: inherit;
cM('target', `
border-top-right-radius: var(--n-border-radius);
border-bottom-right-radius: var(--n-border-radius);
`, [
cE('border', 'border-left: none;')
]),
cE('border', `
padding: 0 12px;
border: 1px solid var(--n-border-color);
transition: border-color .3s var(--n-bezier);
pointer-events: none;
border-radius: inherit;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
`),
cB('transfer-list-header', `
min-height: var(--n-header-height);
box-sizing: border-box;
display: flex;
padding: 12px 12px 10px 12px;
align-items: center;
background-clip: padding-box;
border-radius: inherit;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
line-height: 1.5;
transition:
border-color .3s var(--n-bezier),
background-color .3s var(--n-bezier);
`, [
c('> *:not(:first-child)', `
margin-left: 8px;
`),
cE('title', `
flex: 1;
min-width: 0;
line-height: 1.5;
font-size: var(--n-header-font-size);
font-weight: var(--n-header-font-weight);
transition: color .3s var(--n-bezier);
color: var(--n-header-text-color);
`),
cE('button', `
position: relative;
`),
cE('extra', `
transition: color .3s var(--n-bezier);
font-size: var(--n-extra-font-size);
margin-right: 0;
white-space: nowrap;
color: var(--n-header-extra-text-color);
`)
]),
cB('transfer-list-body', `
flex-basis: 0;
flex-grow: 1;
box-sizing: border-box;
position: relative;
display: flex;
flex-direction: column;
background-clip: padding-box;
width: calc(50% - 36px);
position: relative;
transition: background-color .3s var(--n-bezier);
border-radius: var(--n-border-radius);
background-color: var(--n-list-color);
border-radius: inherit;
border-top-left-radius: 0;
border-top-right-radius: 0;
`, [
c('&:first-child', `
border-top-right-radius: 0!important;
border-bottom-right-radius: 0!important;
`),
c('&:last-child', `
margin-left: -1px!important;
border-top-left-radius: 0!important;
border-bottom-left-radius: 0!important;
`, [
cB('transfer-list-header', null, [
cE('header', `
flex: 1;
line-height: 1;
font-weight: var(--n-header-font-weight);
transition: color .3s var(--n-bezier);
color: var(--n-header-text-color);
padding-left: 14px;
`, [
cM('disabled', {
color: 'var(--n-header-text-color-disabled)'
})
])
]),
cB('transfer-list-item', null, [
cE('label', `
padding-left: 14px;
`)
])
]),
cE('border', `
border: 1px solid var(--n-border-color);
transition: border-color .3s var(--n-bezier);
pointer-events: none;
border-radius: inherit;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
`),
cB('transfer-list-header', `
height: calc(var(--n-item-height) + 4px);
cB('transfer-filter', `
padding: 4px 12px 8px 12px;
box-sizing: border-box;
display: grid;
grid-template-areas: "button . extra";
grid-template-columns: auto 1fr auto;
align-items: center;
background-clip: padding-box;
border-radius: inherit;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
transition:
border-color .3s var(--n-bezier),
background-color .3s var(--n-bezier);
`, [
cE('button', `
grid-area: button;
position: relative;
padding: 0 9px 0 14px;
`),
cE('extra', `
grid-area: extra;
transition: color .3s var(--n-bezier);
font-size: var(--n-extra-font-size);
margin-right: 14px;
white-space: nowrap;
color: var(--n-header-extra-text-color);
`)
]),
cB('transfer-list-body', `
flex-basis: 0;
flex-grow: 1;
box-sizing: border-box;
`),
cB('transfer-list-flex-container', `
flex: 1;
position: relative;
display: flex;
flex-direction: column;
border-radius: inherit;
border-top-left-radius: 0;
border-top-right-radius: 0;
`, [
cB('transfer-filter', `
padding: 2px 8px 8px 8px;
box-sizing: border-box;
transition:
border-color .3s var(--n-bezier),
background-color .3s var(--n-bezier);
cB('scrollbar', `
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
height: unset;
`),
cB('transfer-list-flex-container', `
flex: 1;
cB('empty', `
position: absolute;
left: 50%;
top: 50%;
transform: translateY(-50%) translateX(-50%);
`),
cB('transfer-list-content', `
padding: 0;
margin: 0;
position: relative;
`, [
cB('scrollbar', `
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
height: unset;
`, [
cB('scrollbar-content', {
width: '100%'
})
]),
cB('empty', `
position: absolute;
left: 50%;
top: 50%;
transform: translateY(-50%) translateX(-50%);
`),
cB('transfer-list-content', `
padding: 0;
margin: 0;
cB('transfer-list-item', `
padding: 0 12px;
min-height: var(--n-item-height);
display: flex;
align-items: center;
color: var(--n-item-text-color);
position: relative;
transition: color .3s var(--n-bezier);
`, [
cM('transition-disabled', [
cB('transfer-list-item', {
animation: 'none !important'
})
]),
cB('transfer-list-item', `
min-height: var(--n-item-height);
display: grid;
grid-template-areas: "checkbox label suffix";
grid-template-columns: auto 1fr auto;
align-items: center;
color: var(--n-item-text-color);
`, [
cNotM('disabled', [
c('&:hover', {
backgroundColor: 'var(--n-item-color-pending)'
})
]),
cE('checkbox', `
grid-area: checkbox;
padding: 0 9px 0 14px;
`),
cE('close', `
grid-area: suffix;
padding: 4px 14px 0 9px;
`),
cM('disabled', `
cursor: not-allowed
background-color: #0000;
color: var(--n-item-text-color-disabled);
`),
cM('source', `
cursor: pointer;
`),
cM('target', `
cursor: default;
`)
cE('background', `
position: absolute;
left: 4px;
right: 4px;
top: 0;
bottom: 0;
border-radius: var(--n-border-radius);
transition: background-color .3s var(--n-bezier);
`),
cE('checkbox', `
position: relative;
margin-right: 8px;
`),
cE('close', `
opacity: 0;
pointer-events: none;
position: relative;
transition:
opacity .3s var(--n-bezier),
background-color .3s var(--n-bezier),
color .3s var(--n-bezier);
`),
cE('label', `
position: relative;
min-width: 0;
flex-grow: 1;
`),
cM('source', 'cursor: pointer;'),
cM('disabled', `
cursor: not-allowed;
color: var(--n-item-text-color-disabled);
`),
cNotM('disabled', [
c('&:hover', [
cE('background', 'background-color: var(--n-item-color-pending);'),
cE('close', `
opacity: 1;
pointer-events: all;
`)
])
])
])
])
])
])
]),
animation
])
])

View File

@ -1,6 +1,6 @@
import { ref, computed, toRef } from 'vue'
import { useMergedState } from 'vooks'
import type { Option, OptionValue, Filter, CheckedStatus } from './interface'
import type { Option, OptionValue, Filter } from './interface'
interface UseTransferDataProps {
defaultValue: OptionValue[] | null
@ -13,87 +13,91 @@ interface UseTransferDataProps {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function useTransferData (props: UseTransferDataProps) {
const uncontrolledValueRef = ref(props.defaultValue)
const controlledValueRef = toRef(props, 'value')
const mergedValueRef = useMergedState(
controlledValueRef,
toRef(props, 'value'),
uncontrolledValueRef
)
// map 化的 options
const optMapRef = computed(() => {
const optionsMapRef = computed(() => {
const map = new Map()
;(props.options || []).forEach((opt) => map.set(opt.value, opt))
return map as Map<OptionValue, Option>
})
// set 化的 value
const tgtValueSetRef = computed(() => new Set(mergedValueRef.value || []))
const targetValueSetRef = computed(() => new Set(mergedValueRef.value || []))
// 用于展示源项列表数目
const srcOptsRef = computed(() => props.options)
// 用于展示目标项列表数目
const tgtOptsRef = computed(() => {
const optMap = optMapRef.value
return (mergedValueRef.value || []).map((v) => optMap.get(v)) as Option[]
const targetOptionsRef = computed(() => {
const optionMap = optionsMapRef.value
return (mergedValueRef.value || []).map((v) => optionMap.get(v)) as Option[]
})
// 源项过滤输入的值
const srcPatternRef = ref('')
// 被过滤后的源项列表
const filteredSrcOptsRef = computed(() => {
const filteredSrcOptionsRef = computed(() => {
if (!props.filterable) return props.options
const { filter } = props
return props.options.filter((opt) =>
filter(srcPatternRef.value, opt, 'source')
return props.options.filter((opt) => filter(srcPatternRef.value, opt))
})
const mergedValueSetRef = computed<Set<string | number>>(() => {
const { value } = mergedValueRef
if (value === null) return new Set()
return new Set(value)
})
const valueSetForSelectAllRef = computed(() => {
const mergedValueSet = mergedValueSetRef.value
return new Set(
filteredSrcOptionsRef.value
.filter(
(option) => !option.disabled || mergedValueSet.has(option.value)
)
.map((option) => option.value)
)
})
// 没有被禁用的源项列表
const avlSrcValueSetRef = computed(
const valueSetForUnselectAllRef = computed(
() =>
new Set(
filteredSrcOptsRef.value
.filter((opt) => !opt.disabled)
.map((opt) => opt.value)
targetOptionsRef.value
.filter((option) => option.disabled)
.map((option) => option.value)
)
)
// 用于头部按钮状态
const headerBtnStatusRef = computed<CheckedStatus>(() => {
const checkedLength = mergedValueRef.value?.length
const avlValueCount = avlSrcValueSetRef.value.size
return {
checked: !!checkedLength,
allChecked: checkedLength === avlValueCount,
disabled: !avlValueCount
}
const canNotSelectAnythingRef = computed(() => {
return filteredSrcOptionsRef.value.every((option) => option.disabled)
})
const isInputingRef = ref(false)
function handleInputFocus (): void {
isInputingRef.value = true
}
function handleInputBlur (): void {
isInputingRef.value = false
}
const allCheckedRef = computed(() => {
if (!filteredSrcOptionsRef.value.length) {
return false
}
const mergedValueSet = mergedValueSetRef.value
return filteredSrcOptionsRef.value.every(
(option) => option.disabled || mergedValueSet.has(option.value)
)
})
const canBeClearedRef = computed(() => {
return targetOptionsRef.value.some((option) => !option.disabled)
})
function handleSrcFilterUpdateValue (value: string | null): void {
srcPatternRef.value = value ?? ''
}
return {
uncontrolledValue: uncontrolledValueRef,
mergedValue: mergedValueRef,
tgtValueSet: tgtValueSetRef,
avlSrcValueSet: avlSrcValueSetRef,
tgtOpts: tgtOptsRef,
srcOpts: srcOptsRef,
filteredSrcOpts: filteredSrcOptsRef,
headerBtnStatus: headerBtnStatusRef,
srcPattern: srcPatternRef,
isInputing: isInputingRef,
handleInputFocus,
handleInputBlur,
uncontrolledValueRef,
mergedValueRef,
targetValueSetRef,
valueSetForSelectAllRef,
valueSetForUnselectAllRef,
targetOptionsRef,
filteredSrcOptionsRef,
canNotSelectAnythingRef,
canBeClearedRef,
allCheckedRef,
srcPatternRef,
handleSrcFilterUpdateValue
}
}

View File

@ -1,4 +1,13 @@
export default {
extraFontSize: '12px',
width: '440px'
extraFontSizeSmall: '12px',
extraFontSizeMedium: '12px',
extraFontSizeLarge: '14px',
titleFontSizeSmall: '14px',
titleFontSizeMedium: '16px',
titleFontSizeLarge: '16px',
closeSize: '20px',
closeIconSize: '16px',
headerHeightSmall: '44px',
headerHeightMedium: '44px',
headerHeightLarge: '50px'
}

View File

@ -19,45 +19,53 @@ const transferDark: TransferTheme = {
},
self (vars) {
const {
iconColorDisabled,
iconColor,
fontWeight,
fontSizeLarge,
fontSizeMedium,
fontSizeSmall,
heightLarge,
heightMedium,
heightSmall,
borderRadius,
inputColor,
tableHeaderColor,
textColor1,
textColorDisabled,
textColor2,
hoverColor
textColor3,
hoverColor,
closeColorHover,
closeColorPressed,
closeIconColor,
closeIconColorHover,
closeIconColorPressed,
dividerColor
} = vars
return {
...commonVariables,
itemHeightSmall: heightSmall,
itemHeightSmall: heightMedium,
itemHeightMedium: heightMedium,
itemHeightLarge: heightLarge,
fontSizeSmall,
fontSizeMedium,
fontSizeLarge,
borderRadius,
dividerColor,
borderColor: '#0000',
listColor: inputColor,
headerColor: tableHeaderColor,
titleTextColor: textColor1,
titleTextColorDisabled: textColorDisabled,
extraTextColor: textColor2,
filterDividerColor: '#0000',
extraTextColor: textColor3,
extraTextColorDisabled: textColorDisabled,
itemTextColor: textColor2,
itemTextColorDisabled: textColorDisabled,
itemColorPending: hoverColor,
titleFontWeight: fontWeight,
iconColor,
iconColorDisabled
closeColorHover,
closeColorPressed,
closeIconColor,
closeIconColorHover,
closeIconColorPressed
}
}
}

View File

@ -12,45 +12,52 @@ import { createTheme } from '../../_mixins'
const self = (vars: ThemeCommonVars) => {
const {
fontWeight,
iconColorDisabled,
iconColor,
fontSizeLarge,
fontSizeMedium,
fontSizeSmall,
heightLarge,
heightMedium,
heightSmall,
borderRadius,
cardColor,
tableHeaderColor,
textColor1,
textColorDisabled,
textColor2,
textColor3,
borderColor,
hoverColor
hoverColor,
closeColorHover,
closeColorPressed,
closeIconColor,
closeIconColorHover,
closeIconColorPressed
} = vars
return {
...commonVariables,
itemHeightSmall: heightSmall,
itemHeightSmall: heightMedium,
itemHeightMedium: heightMedium,
itemHeightLarge: heightLarge,
fontSizeSmall,
fontSizeMedium,
fontSizeLarge,
borderRadius,
dividerColor: borderColor,
borderColor,
listColor: cardColor,
headerColor: composite(cardColor, tableHeaderColor),
titleTextColor: textColor1,
titleTextColorDisabled: textColorDisabled,
extraTextColor: textColor2,
filterDividerColor: borderColor,
extraTextColor: textColor3,
extraTextColorDisabled: textColorDisabled,
itemTextColor: textColor2,
itemTextColorDisabled: textColorDisabled,
itemColorPending: hoverColor,
titleFontWeight: fontWeight,
iconColor,
iconColorDisabled
closeColorHover,
closeColorPressed,
closeIconColor,
closeIconColorHover,
closeIconColorPressed
}
}

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`n-transfer should work with \`size\` prop 1`] = `"--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-border-color: rgb(224, 224, 230); --n-border-radius: 3px; --n-extra-font-size: 12px; --n-font-size: 14px; --n-header-extra-text-color: rgb(51, 54, 57); --n-header-font-weight: 400; --n-header-text-color: rgb(31, 34, 37); --n-header-text-color-disabled: rgba(194, 194, 194, 1); --n-item-color-pending: rgb(243, 243, 245); --n-item-height: 28px; --n-item-text-color: rgb(51, 54, 57); --n-item-text-color-disabled: rgba(194, 194, 194, 1); --n-list-color: #fff; --n-width: 440px; --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1);"`;
exports[`n-transfer should work with \`size\` prop 1`] = `"--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-border-color: rgb(224, 224, 230); --n-border-radius: 3px; --n-extra-font-size: 12px; --n-font-size: 14px; --n-header-font-size: 14px; --n-header-extra-text-color: rgb(118, 124, 130); --n-header-extra-text-color-disabled: rgba(194, 194, 194, 1); --n-header-font-weight: 400; --n-header-text-color: rgb(31, 34, 37); --n-header-text-color-disabled: rgba(194, 194, 194, 1); --n-item-color-pending: rgb(243, 243, 245); --n-item-height: 34px; --n-item-text-color: rgb(51, 54, 57); --n-item-text-color-disabled: rgba(194, 194, 194, 1); --n-list-color: #fff; --n-header-height: 44px; --n-close-size: 20px; --n-close-icon-size: 16px; --n-close-color-hover: rgba(0, 0, 0, .09); --n-close-color-pressed: rgba(0, 0, 0, .13); --n-close-icon-color: rgba(102, 102, 102, 1); --n-close-icon-color-hover: rgba(102, 102, 102, 1); --n-close-icon-color-pressed: rgba(102, 102, 102, 1); --n-divider-color: rgb(224, 224, 230);"`;
exports[`n-transfer should work with \`size\` prop 2`] = `"--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-border-color: rgb(224, 224, 230); --n-border-radius: 3px; --n-extra-font-size: 12px; --n-font-size: 14px; --n-header-extra-text-color: rgb(51, 54, 57); --n-header-font-weight: 400; --n-header-text-color: rgb(31, 34, 37); --n-header-text-color-disabled: rgba(194, 194, 194, 1); --n-item-color-pending: rgb(243, 243, 245); --n-item-height: 34px; --n-item-text-color: rgb(51, 54, 57); --n-item-text-color-disabled: rgba(194, 194, 194, 1); --n-list-color: #fff; --n-width: 440px; --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1);"`;
exports[`n-transfer should work with \`size\` prop 2`] = `"--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-border-color: rgb(224, 224, 230); --n-border-radius: 3px; --n-extra-font-size: 12px; --n-font-size: 14px; --n-header-font-size: 16px; --n-header-extra-text-color: rgb(118, 124, 130); --n-header-extra-text-color-disabled: rgba(194, 194, 194, 1); --n-header-font-weight: 400; --n-header-text-color: rgb(31, 34, 37); --n-header-text-color-disabled: rgba(194, 194, 194, 1); --n-item-color-pending: rgb(243, 243, 245); --n-item-height: 34px; --n-item-text-color: rgb(51, 54, 57); --n-item-text-color-disabled: rgba(194, 194, 194, 1); --n-list-color: #fff; --n-header-height: 44px; --n-close-size: 20px; --n-close-icon-size: 16px; --n-close-color-hover: rgba(0, 0, 0, .09); --n-close-color-pressed: rgba(0, 0, 0, .13); --n-close-icon-color: rgba(102, 102, 102, 1); --n-close-icon-color-hover: rgba(102, 102, 102, 1); --n-close-icon-color-pressed: rgba(102, 102, 102, 1); --n-divider-color: rgb(224, 224, 230);"`;
exports[`n-transfer should work with \`size\` prop 3`] = `"--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-border-color: rgb(224, 224, 230); --n-border-radius: 3px; --n-extra-font-size: 12px; --n-font-size: 15px; --n-header-extra-text-color: rgb(51, 54, 57); --n-header-font-weight: 400; --n-header-text-color: rgb(31, 34, 37); --n-header-text-color-disabled: rgba(194, 194, 194, 1); --n-item-color-pending: rgb(243, 243, 245); --n-item-height: 40px; --n-item-text-color: rgb(51, 54, 57); --n-item-text-color-disabled: rgba(194, 194, 194, 1); --n-list-color: #fff; --n-width: 440px; --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1);"`;
exports[`n-transfer should work with \`size\` prop 3`] = `"--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-border-color: rgb(224, 224, 230); --n-border-radius: 3px; --n-extra-font-size: 14px; --n-font-size: 15px; --n-header-font-size: 16px; --n-header-extra-text-color: rgb(118, 124, 130); --n-header-extra-text-color-disabled: rgba(194, 194, 194, 1); --n-header-font-weight: 400; --n-header-text-color: rgb(31, 34, 37); --n-header-text-color-disabled: rgba(194, 194, 194, 1); --n-item-color-pending: rgb(243, 243, 245); --n-item-height: 40px; --n-item-text-color: rgb(51, 54, 57); --n-item-text-color-disabled: rgba(194, 194, 194, 1); --n-list-color: #fff; --n-header-height: 50px; --n-close-size: 20px; --n-close-icon-size: 16px; --n-close-color-hover: rgba(0, 0, 0, .09); --n-close-color-pressed: rgba(0, 0, 0, .13); --n-close-icon-color: rgba(102, 102, 102, 1); --n-close-icon-color-hover: rgba(102, 102, 102, 1); --n-close-icon-color-pressed: rgba(102, 102, 102, 1); --n-divider-color: rgb(224, 224, 230);"`;

1
volar.d.ts vendored
View File

@ -139,6 +139,7 @@ declare module 'vue' {
NUploadFileList: typeof import('naive-ui')['NUploadFileList']
NUploadTrigger: typeof import('naive-ui')['NUploadTrigger']
NWatermark: typeof import('naive-ui')['NWatermark']
NLegacyTransfer: typeof import('naive-ui')['NLegacyTransfer']
}
}
export {}