refactor(transfer): move to legacy-transfer

This commit is contained in:
07akioni 2022-07-24 16:27:26 +08:00
parent 039f5ef2e7
commit 9b260a3b16
28 changed files with 1967 additions and 16 deletions

View File

@ -333,6 +333,11 @@ export const enComponentRoutes = [
path: 'transfer',
component: () => import('../../src/transfer/demos/enUS/index.demo-entry.md')
},
{
path: 'legacy-transfer',
component: () =>
import('../../src/legacy-transfer/demos/enUS/index.demo-entry.md')
},
{
path: 'spin',
component: () => import('../../src/spin/demos/enUS/index.demo-entry.md')
@ -701,6 +706,11 @@ export const zhComponentRoutes = [
path: 'transfer',
component: () => import('../../src/transfer/demos/zhCN/index.demo-entry.md')
},
{
path: 'legacy-transfer',
component: () =>
import('../../src/legacy-transfer/demos/zhCN/index.demo-entry.md')
},
{
path: 'spin',
component: () => import('../../src/spin/demos/zhCN/index.demo-entry.md')

View File

@ -32,12 +32,6 @@ const appendCounts = (item) => {
}
}
// const createDebugDemos = (item, mode) => {
// if (__DEV__ && mode === 'debug') {
// return [item]
// } else return []
// }
function createItems (lang, theme, prefix, items) {
const isZh = lang === 'zh-CN'
const langKey = isZh ? 'zh' : 'en'
@ -159,11 +153,6 @@ export function createDocumentationMenuOptions ({ lang, theme, mode }) {
zh: '社区精选资源',
path: '/community'
}
// {
// en: 'Experimental Features',
// zh: '试验性特性',
// path: '/experimental-features'
// }
]
},
{
@ -176,11 +165,6 @@ export function createDocumentationMenuOptions ({ lang, theme, mode }) {
zh: '变更日志',
path: '/changelog'
}
// {
// en: 'Migrate From V1',
// zh: '从 V1 升级',
// path: '/from-v1'
// }
]
}
])
@ -747,6 +731,19 @@ export function createComponentMenuOptions ({ lang, theme, mode }) {
path: '/global-style'
}
]
}),
appendCounts({
zh: '废弃的组件',
en: 'Deprecated Components',
type: 'group',
children: [
{
en: 'Legacy Transfer',
zh: '旧版穿梭框',
enSuffix: true,
path: '/legacy-transfer'
}
]
})
])
}

View File

@ -0,0 +1,34 @@
<markdown>
# Basic
Basic example of the Transfer component. If you have tons of data, see below for virtualised lists.
</markdown>
<template>
<n-transfer ref="transfer" v-model:value="value" :options="options" />
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
function createOptions () {
return Array.from({ length: 100 }).map((v, i) => ({
label: 'Option ' + i,
value: i,
disabled: i % 5 === 0
}))
}
function createValues () {
return Array.from({ length: 50 }).map((v, i) => i)
}
export default defineComponent({
setup () {
return {
options: createOptions(),
value: ref(createValues())
}
}
})
</script>

View File

@ -0,0 +1,38 @@
<markdown>
# Filterable
</markdown>
<template>
<n-transfer
ref="transfer"
v-model:value="value"
virtual-scroll
:options="options"
filterable
/>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
function createOptions () {
return Array.from({ length: 100 }).map((v, i) => ({
label: 'Option ' + i,
value: i,
disabled: i % 5 === 0
}))
}
function createValues () {
return Array.from({ length: 50 }).map((v, i) => i)
}
export default defineComponent({
setup () {
return {
options: createOptions(),
value: ref(createValues())
}
}
})
</script>

View File

@ -0,0 +1,46 @@
# Legacy Transfer
<!--single-column-->
<n-alert title="Warning">
The transfer component is deprecated and will be removed in the next major version.
</n-alert>
Left, right, right, left... I'm a simple man, and I can play this all day.
## Demos
```demo
basic.vue
large-data.vue
size.vue
filterable.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. |
| 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
| Property | Type | Description |
| -------- | ------------------ | ------------------------------ |
| label | `string` | The option's label to display. |
| value | `string \| number` | The option's unique value. |
| disabled | `boolean` | The option's disabled state. |

View File

@ -0,0 +1,39 @@
<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).
</markdown>
<template>
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
virtual-scroll
/>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
function createOptions () {
return Array.from({ length: 42000 }).map((v, i) => ({
label: 'Option' + i,
value: i,
disabled: i % 5 === 0
}))
}
function createValues () {
return Array.from({ length: 50 }).map((v, i) => i)
}
export default defineComponent({
setup () {
return {
options: createOptions(),
value: ref(createValues())
}
}
})
</script>

View File

@ -0,0 +1,47 @@
<markdown>
# Size
Mixing sizes does not look harmonious.
</markdown>
<template>
<n-space vertical>
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
size="small"
/>
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
size="large"
/>
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
function createOptions () {
return Array.from({ length: 100 }).map((v, i) => ({
label: 'Option ' + i,
value: i,
disabled: i % 5 === 0
}))
}
function createValues () {
return Array.from({ length: 50 }).map((v, i) => i)
}
export default defineComponent({
setup () {
return {
options: createOptions(),
value: ref(createValues())
}
}
})
</script>

View File

@ -0,0 +1,34 @@
<markdown>
# 基础用法
穿梭框的基础用法如果你有一大堆数据看下一个例子
</markdown>
<template>
<n-transfer ref="transfer" v-model:value="value" :options="options" />
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
function createOptions () {
return Array.from({ length: 100 }).map((v, i) => ({
label: 'Option ' + i,
value: i,
disabled: i % 5 === 0
}))
}
function createValues () {
return Array.from({ length: 50 }).map((v, i) => i)
}
export default defineComponent({
setup () {
return {
options: createOptions(),
value: ref(createValues())
}
}
})
</script>

View File

@ -0,0 +1,38 @@
<markdown>
# 可过滤
</markdown>
<template>
<n-transfer
ref="transfer"
v-model:value="value"
virtual-scroll
:options="options"
filterable
/>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
function createOptions () {
return Array.from({ length: 100 }).map((v, i) => ({
label: 'Option ' + i,
value: i,
disabled: i % 5 === 0
}))
}
function createValues () {
return Array.from({ length: 50 }).map((v, i) => i)
}
export default defineComponent({
setup () {
return {
options: createOptions(),
value: ref(createValues())
}
}
})
</script>

View File

@ -0,0 +1,46 @@
# 旧版穿梭框 Legacy Transfer
<!--single-column-->
<n-alert title="警告">
这个穿梭框组件已经被废弃,并将在下一个大版本中彻底移除。
</n-alert>
左、右、左、右...像我这么无聊的人能玩一整天。
## 演示
```demo
basic.vue
large-data.vue
size.vue
filterable.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 |
| 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
| 属性 | 类型 | 说明 |
| -------- | ------------------ | ------------------------ |
| label | `string` | 选项中用以页面显示的名称 |
| value | `string \| number` | 所有选项中唯一的 `value` |
| disabled | `boolean` | 是否禁用这个选项 |

View File

@ -0,0 +1,39 @@
<markdown>
# 一大堆数据
如果你有一大堆数据你可能想让它快一点设定 `virtual-scroll` 来使用一个飞快的穿梭框会关掉那个傻乎乎的动画
</markdown>
<template>
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
virtual-scroll
/>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
function createOptions () {
return Array.from({ length: 42000 }).map((v, i) => ({
label: 'Option ' + i,
value: i,
disabled: i % 5 === 0
}))
}
function createValues () {
return Array.from({ length: 50 }).map((v, i) => i)
}
export default defineComponent({
setup () {
return {
options: createOptions(),
value: ref(createValues())
}
}
})
</script>

View File

@ -0,0 +1,47 @@
<markdown>
# 尺寸
太小太大好像都不怎么好看
</markdown>
<template>
<n-space vertical>
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
size="small"
/>
<n-transfer
ref="transfer"
v-model:value="value"
:options="options"
size="large"
/>
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
function createOptions () {
return Array.from({ length: 100 }).map((v, i) => ({
label: 'Option ' + i,
value: i,
disabled: i % 5 === 0
}))
}
function createValues () {
return Array.from({ length: 50 }).map((v, i) => i)
}
export default defineComponent({
setup () {
return {
options: createOptions(),
value: ref(createValues())
}
}
})
</script>

View File

@ -0,0 +1,6 @@
export {
default as NLegacyTransfer,
transferProps as legacyTransferProps
} from './src/Transfer'
export type { transferProps as LegacyTransferProps } from './src/Transfer'
export type { Option as LegacyTransferOption } from './src/interface'

View File

@ -0,0 +1,402 @@
import {
computed,
defineComponent,
h,
provide,
PropType,
CSSProperties
} from 'vue'
import { useIsMounted } from 'vooks'
import { depx } from 'seemly'
import { ChevronLeftIcon, ChevronRightIcon } from '../../_internal/icons'
import { NBaseIcon } from '../../_internal'
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 type { MaybeArray } from '../../_utils'
import { transferLight } from '../styles'
import type { TransferTheme } from '../styles'
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
} from './interface'
export const transferProps = {
...(useTheme.props as ThemeProps<TransferTheme>),
value: Array as PropType<OptionValue[] | null>,
defaultValue: {
type: Array as PropType<OptionValue[] | null>,
default: null
},
options: {
type: Array as PropType<Option[]>,
default: () => []
},
disabled: {
type: Boolean as PropType<boolean | undefined>,
default: undefined
},
virtualScroll: Boolean,
sourceTitle: String,
targetTitle: String,
filterable: Boolean,
sourceFilterPlaceholder: String,
targetFilterPlaceholder: String,
filter: {
type: Function as PropType<Filter>,
default: (pattern: string, option: Option) => {
if (!pattern) return true
return ~('' + option.label)
.toLowerCase()
.indexOf(('' + pattern).toLowerCase())
}
},
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
}
} as const
export type TransferProps = ExtractPublicPropTypes<typeof transferProps>
export default defineComponent({
name: 'Transfer',
props: transferProps,
setup (props) {
const { mergedClsPrefixRef } = useConfig(props)
const themeRef = useTheme(
'Transfer',
'-transfer',
style,
transferLight,
props,
mergedClsPrefixRef
)
const formItem = useFormItem(props)
const { mergedSizeRef, mergedDisabledRef } = formItem
const itemSizeRef = computed(() => {
const { value: size } = mergedSizeRef
const {
self: { [createKey('itemHeight', size)]: itemSize }
} = themeRef.value
return depx(itemSize)
})
const {
uncontrolledValue: uncontrolledValueRef,
mergedValue: mergedValueRef,
avlSrcValueSet: avlSrcValueSetRef,
avlTgtValueSet: avlTgtValueSetRef,
tgtOpts: tgtOptsRef,
srcOpts: srcOptsRef,
filteredSrcOpts: filteredSrcOptsRef,
filteredTgtOpts: filteredTgtOptsRef,
srcCheckedValues: srcCheckedValuesRef,
tgtCheckedValues: tgtCheckedValuesRef,
srcCheckedStatus: srcCheckedStatusRef,
tgtCheckedStatus: tgtCheckedStatusRef,
srcPattern: srcPatternRef,
tgtPattern: tgtPatternRef,
isInputing: isInputingRef,
fromButtonDisabled: fromButtonDisabledRef,
toButtonDisabled: toButtonDisabledRef,
handleInputFocus,
handleInputBlur,
handleTgtFilterUpdateValue,
handleSrcFilterUpdateValue
} = useTransferData(props, mergedDisabledRef)
function doUpdateValue (value: OptionValue[]): void {
const {
onUpdateValue,
'onUpdate:value': _onUpdateValue,
onChange
} = props
const { nTriggerFormInput, nTriggerFormChange } = formItem
if (onUpdateValue) call(onUpdateValue, value)
if (_onUpdateValue) call(_onUpdateValue, value)
if (onChange) call(onChange, value)
uncontrolledValueRef.value = value
nTriggerFormInput()
nTriggerFormChange()
}
function handleSrcHeaderCheck (value: boolean): void {
const {
value: { checked, indeterminate }
} = srcCheckedStatusRef
if (indeterminate || checked) {
srcCheckedValuesRef.value = []
} else {
srcCheckedValuesRef.value = Array.from(avlSrcValueSetRef.value)
}
}
function handleTgtHeaderCheck (): void {
const {
value: { checked, indeterminate }
} = tgtCheckedStatusRef
if (indeterminate || checked) {
tgtCheckedValuesRef.value = []
} else {
tgtCheckedValuesRef.value = Array.from(avlTgtValueSetRef.value)
}
}
function handleTgtCheckboxClick (
checked: boolean,
optionValue: OptionValue
): void {
if (checked) {
tgtCheckedValuesRef.value.push(optionValue)
} else {
const index = tgtCheckedValuesRef.value.findIndex(
(v) => v === optionValue
)
if (~index) {
tgtCheckedValuesRef.value.splice(index, 1)
}
}
}
function handleSrcCheckboxClick (
checked: boolean,
optionValue: OptionValue
): void {
if (checked) {
srcCheckedValuesRef.value.push(optionValue)
} else {
const index = srcCheckedValuesRef.value.findIndex(
(v) => v === optionValue
)
if (~index) {
srcCheckedValuesRef.value.splice(index, 1)
}
}
}
function handleToTgtClick (): void {
doUpdateValue(
srcCheckedValuesRef.value.concat(mergedValueRef.value || [])
)
srcCheckedValuesRef.value = []
}
function handleToSrcClick (): void {
const tgtCheckedValueSet = new Set(tgtCheckedValuesRef.value)
doUpdateValue(
(mergedValueRef.value || []).filter((v) => !tgtCheckedValueSet.has(v))
)
tgtCheckedValuesRef.value = []
}
provide(transferInjectionKey, {
mergedClsPrefixRef,
mergedSizeRef,
disabledRef: mergedDisabledRef,
mergedThemeRef: themeRef,
srcCheckedValuesRef,
tgtCheckedValuesRef,
srcOptsRef,
tgtOptsRef,
srcCheckedStatusRef,
tgtCheckedStatusRef,
handleSrcCheckboxClick,
handleTgtCheckboxClick
})
const { localeRef } = useLocale('Transfer')
return {
locale: localeRef,
mergedClsPrefix: mergedClsPrefixRef,
mergedDisabled: mergedDisabledRef,
itemSize: itemSizeRef,
isMounted: useIsMounted(),
isInputing: isInputingRef,
mergedTheme: themeRef,
filteredSrcOpts: filteredSrcOptsRef,
filteredTgtOpts: filteredTgtOptsRef,
srcPattern: srcPatternRef,
tgtPattern: tgtPatternRef,
toButtonDisabled: toButtonDisabledRef,
fromButtonDisabled: fromButtonDisabledRef,
handleSrcHeaderCheck,
handleTgtHeaderCheck,
handleToSrcClick,
handleToTgtClick,
handleInputFocus,
handleInputBlur,
handleTgtFilterUpdateValue,
handleSrcFilterUpdateValue,
cssVars: computed(() => {
const { value: size } = mergedSizeRef
const {
common: {
cubicBezierEaseInOut,
cubicBezierEaseIn,
cubicBezierEaseOut
},
self: {
width,
borderRadius,
borderColor,
listColor,
headerColor,
titleTextColor,
titleTextColorDisabled,
extraTextColor,
filterDividerColor,
itemTextColor,
itemColorPending,
itemTextColorDisabled,
extraFontSize,
titleFontWeight,
iconColor,
iconColorDisabled,
[createKey('fontSize', size)]: fontSize,
[createKey('itemHeight', size)]: itemHeight
}
} = themeRef.value
return {
'--n-bezier': cubicBezierEaseInOut,
'--n-bezier-ease-in': cubicBezierEaseIn,
'--n-bezier-ease-out': cubicBezierEaseOut,
'--n-border-color': borderColor,
'--n-border-radius': borderRadius,
'--n-extra-font-size': extraFontSize,
'--n-filter-divider-color': filterDividerColor,
'--n-font-size': fontSize,
'--n-header-color': headerColor,
'--n-header-extra-text-color': extraTextColor,
'--n-header-font-weight': titleFontWeight,
'--n-header-text-color': titleTextColor,
'--n-header-text-color-disabled': titleTextColorDisabled,
'--n-item-color-pending': itemColorPending,
'--n-item-height': itemHeight,
'--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
}
})
}
},
render () {
const { mergedClsPrefix } = this
return (
<div
class={[
`${mergedClsPrefix}-transfer`,
this.mergedDisabled && `${mergedClsPrefix}-transfer--disabled`,
this.filterable && `${mergedClsPrefix}-transfer--filterable`
]}
style={this.cssVars as CSSProperties}
>
<div class={`${mergedClsPrefix}-transfer-list`}>
<NTransferHeader
source
onChange={this.handleSrcHeaderCheck}
title={this.sourceTitle || this.locale.sourceTitle}
/>
<div class={`${mergedClsPrefix}-transfer-list-body`}>
{this.filterable ? (
<NTransferFilter
onUpdateValue={this.handleSrcFilterUpdateValue}
value={this.srcPattern}
disabled={this.mergedDisabled}
placeholder={this.sourceFilterPlaceholder}
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}
/>
) : null}
<div class={`${mergedClsPrefix}-transfer-list-flex-container`}>
<NTransferList
source
options={this.filteredSrcOpts}
disabled={this.mergedDisabled}
virtualScroll={this.virtualScroll}
isMounted={this.isMounted}
isInputing={this.isInputing}
itemSize={this.itemSize}
/>
</div>
</div>
<div class={`${mergedClsPrefix}-transfer-list__border`} />
</div>
<div class={`${mergedClsPrefix}-transfer-gap`}>
<NButton
disabled={this.toButtonDisabled || this.mergedDisabled}
theme={this.mergedTheme.peers.Button}
themeOverrides={this.mergedTheme.peerOverrides.Button}
onClick={this.handleToTgtClick}
>
{{
icon: () => (
<NBaseIcon clsPrefix={mergedClsPrefix}>
{{ default: () => <ChevronRightIcon /> }}
</NBaseIcon>
)
}}
</NButton>
<NButton
disabled={this.fromButtonDisabled || this.mergedDisabled}
theme={this.mergedTheme.peers.Button}
themeOverrides={this.mergedTheme.peerOverrides.Button}
onClick={this.handleToSrcClick}
>
{{
icon: () => (
<NBaseIcon clsPrefix={mergedClsPrefix}>
{{ default: () => <ChevronLeftIcon /> }}
</NBaseIcon>
)
}}
</NButton>
</div>
<div class={`${mergedClsPrefix}-transfer-list`}>
<NTransferHeader
onChange={this.handleTgtHeaderCheck}
title={this.targetTitle || this.locale.targetTitle}
/>
<div class={`${mergedClsPrefix}-transfer-list-body`}>
{this.filterable ? (
<NTransferFilter
onUpdateValue={this.handleTgtFilterUpdateValue}
value={this.tgtPattern}
disabled={this.mergedDisabled}
placeholder={this.targetFilterPlaceholder}
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}
/>
) : null}
<div class={`${mergedClsPrefix}-transfer-list-flex-container`}>
<NTransferList
options={this.filteredTgtOpts}
disabled={this.mergedDisabled}
virtualScroll={this.virtualScroll}
isMounted={this.isMounted}
isInputing={this.isInputing}
itemSize={this.itemSize}
/>
</div>
</div>
<div class={`${mergedClsPrefix}-transfer-list__border`} />
</div>
</div>
)
}
})

View File

@ -0,0 +1,64 @@
import { h, defineComponent, inject, PropType } from 'vue'
import { SearchIcon } from '../../_internal/icons'
import { NBaseIcon } from '../../_internal'
import { NInput } from '../../input'
import { transferInjectionKey } from './interface'
export default defineComponent({
name: 'TransferFilter',
props: {
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
}
},
setup () {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { mergedThemeRef, mergedClsPrefixRef } = inject(transferInjectionKey)!
return {
mergedClsPrefix: mergedClsPrefixRef,
mergedTheme: mergedThemeRef
}
},
render () {
const { mergedTheme, mergedClsPrefix } = this
return (
<div class={`${mergedClsPrefix}-transfer-filter`}>
<NInput
value={this.value}
onUpdateValue={this.onUpdateValue}
disabled={this.disabled}
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`}
>
{{ default: () => <SearchIcon /> }}
</NBaseIcon>
)
}}
</NInput>
</div>
)
}
})

View File

@ -0,0 +1,69 @@
import { h, computed, defineComponent, inject, PropType } from 'vue'
import { NCheckbox } from '../../checkbox'
import { transferInjectionKey } from './interface'
export default defineComponent({
name: 'TransferHeader',
props: {
source: {
type: Boolean,
default: false
},
onChange: {
type: Function as PropType<(value: boolean) => void>,
required: true
},
title: String
},
setup (props) {
const {
srcOptsRef,
tgtOptsRef,
srcCheckedStatusRef,
tgtCheckedStatusRef,
srcCheckedValuesRef,
tgtCheckedValuesRef,
mergedThemeRef,
disabledRef,
mergedClsPrefixRef
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
} = inject(transferInjectionKey)!
const checkboxPropsRef = computed(() => {
const { source } = props
if (source) {
return srcCheckedStatusRef.value
} else {
return tgtCheckedStatusRef.value
}
})
return () => {
const { source } = props
const { value: checkboxProps } = checkboxPropsRef
const { value: mergedTheme } = mergedThemeRef
const { value: mergedClsPrefix } = mergedClsPrefixRef
return (
<div class={`${mergedClsPrefix}-transfer-list-header`}>
<div class={`${mergedClsPrefix}-transfer-list-header__checkbox`}>
<NCheckbox
theme={mergedTheme.peers.Checkbox}
themeOverrides={mergedTheme.peerOverrides.Checkbox}
checked={checkboxProps.checked}
indeterminate={checkboxProps.indeterminate}
disabled={checkboxProps.disabled || disabledRef.value}
onUpdateChecked={props.onChange}
/>
</div>
<div class={`${mergedClsPrefix}-transfer-list-header__header`}>
{props.title}
</div>
<div class={`${mergedClsPrefix}-transfer-list-header__extra`}>
{source
? srcCheckedValuesRef.value.length
: tgtCheckedValuesRef.value.length}
/{source ? srcOptsRef.value.length : tgtOptsRef.value.length}
</div>
</div>
)
}
}
})

View File

@ -0,0 +1,163 @@
import {
h,
defineComponent,
ref,
inject,
PropType,
TransitionGroup,
Transition,
Fragment
} from 'vue'
import { VirtualList, VirtualListInst } from 'vueuc'
import { NEmpty } from '../../empty'
import { NScrollbar, ScrollbarInst } from '../../_internal'
import { Option, transferInjectionKey } from './interface'
import NTransferListItem from './TransferListItem'
export default defineComponent({
name: 'TransferList',
props: {
virtualScroll: {
type: Boolean,
required: true
},
itemSize: {
type: Number,
required: true
},
options: {
type: Array as PropType<Option[]>,
required: true
},
disabled: {
type: Boolean,
required: true
},
isMounted: {
type: Boolean,
required: true
},
isInputing: {
type: Boolean,
required: true
},
source: {
type: Boolean,
default: false
}
},
setup () {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { mergedThemeRef, mergedClsPrefixRef } = inject(transferInjectionKey)!
const scrollerInstRef = ref<ScrollbarInst | null>(null)
const vlInstRef = ref<VirtualListInst | null>(null)
function syncVLScroller (): void {
scrollerInstRef.value?.sync()
}
function scrollContainer (): HTMLElement | null {
const { value } = vlInstRef
if (!value) return null
const { listElRef } = value
return listElRef
}
function scrollContent (): HTMLElement | null {
const { value } = vlInstRef
if (!value) return null
const { itemsElRef } = value
return itemsElRef
}
return {
mergedTheme: mergedThemeRef,
mergedClsPrefix: mergedClsPrefixRef,
scrollerInstRef,
vlInstRef,
syncVLScroller,
scrollContainer,
scrollContent
}
},
render () {
const { mergedTheme, mergedClsPrefix, virtualScroll, 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}
/>
)
}
}}
</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}
/>
))
}
}}
</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>
</>
)
}
})

View File

@ -0,0 +1,90 @@
import { h, inject, defineComponent } from 'vue'
import { useMemo } from 'vooks'
import { NCheckbox } from '../../checkbox'
import { transferInjectionKey } from './interface'
import { getTitleAttribute } from '../../_utils'
export default defineComponent({
name: 'NTransferListItem',
props: {
source: {
type: Boolean,
default: false
},
label: {
type: String,
required: true
},
value: {
type: [String, Number],
required: true
},
disabled: {
type: Boolean,
default: false
}
},
setup (props) {
const { source } = props
const {
mergedClsPrefixRef,
mergedThemeRef,
srcCheckedValuesRef,
tgtCheckedValuesRef,
handleSrcCheckboxClick,
handleTgtCheckboxClick
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
} = inject(transferInjectionKey)!
const checkedRef = source
? useMemo(() => srcCheckedValuesRef.value.includes(props.value))
: useMemo(() => tgtCheckedValuesRef.value.includes(props.value))
const handleClick = source
? () => {
if (!props.disabled) {
handleSrcCheckboxClick(!checkedRef.value, props.value)
}
}
: () => {
if (!props.disabled) {
handleTgtCheckboxClick(!checkedRef.value, props.value)
}
}
return {
mergedClsPrefix: mergedClsPrefixRef,
mergedTheme: mergedThemeRef,
checked: checkedRef,
handleClick
}
},
render () {
const { disabled, mergedTheme, mergedClsPrefix, label, checked, source } =
this
return (
<div
class={[
`${mergedClsPrefix}-transfer-list-item`,
disabled && `${mergedClsPrefix}-transfer-list-item--disabled`,
source
? `${mergedClsPrefix}-transfer-list-item--source`
: `${mergedClsPrefix}-transfer-list-item--target`
]}
onClick={this.handleClick}
>
<div class={`${mergedClsPrefix}-transfer-list-item__checkbox`}>
<NCheckbox
theme={mergedTheme.peers.Checkbox}
themeOverrides={mergedTheme.peerOverrides.Checkbox}
disabled={disabled}
checked={checked}
/>
</div>
<div
class={`${mergedClsPrefix}-transfer-list-item__label`}
title={getTitleAttribute(label)}
>
{label}
</div>
</div>
)
}
})

View File

@ -0,0 +1,43 @@
import { Ref } from 'vue'
import type { MergedTheme } from '../../_mixins'
import { createInjectionKey } from '../../_utils'
import type { TransferTheme } from '../styles'
export type OptionValue = string | number
export interface Option {
label: string
value: OptionValue
disabled?: boolean
}
export interface CheckedStatus {
checked: boolean
indeterminate: boolean
disabled?: boolean
}
export type Filter = (
pattern: string,
option: Option,
from: 'source' | 'target'
) => boolean
export interface TransferInjection {
mergedClsPrefixRef: Ref<string>
mergedSizeRef: Ref<'small' | 'medium' | 'large'>
disabledRef: Ref<boolean>
mergedThemeRef: Ref<MergedTheme<TransferTheme>>
srcCheckedValuesRef: Ref<OptionValue[]>
tgtCheckedValuesRef: Ref<OptionValue[]>
srcOptsRef: Ref<Option[]>
tgtOptsRef: Ref<Option[]>
srcCheckedStatusRef: Ref<CheckedStatus>
tgtCheckedStatusRef: Ref<CheckedStatus>
handleSrcCheckboxClick: (checked: boolean, value: OptionValue) => void
handleTgtCheckboxClick: (checked: boolean, value: OptionValue) => void
}
export const transferInjectionKey =
createInjectionKey<TransferInjection>('n-transfer')
export type OnUpdateValue = (value: OptionValue[]) => void

View File

@ -0,0 +1,278 @@
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', `
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', `
display: flex;
width: var(--n-width);
font-size: var(--n-font-size);
height: 240px;
display: flex;
flex-wrap: nowrap;
`, [
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)'
})
]),
cB('transfer-list', `
height: inherit;
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);
`, [
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);
box-sizing: border-box;
display: flex;
align-items: center;
background-clip: padding-box;
border-radius: inherit;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background-color: var(--n-header-color);
transition:
border-color .3s var(--n-bezier),
background-color .3s var(--n-bezier);
`, [
cE('checkbox', `
display: flex;
align-items: center;
position: relative;
padding: 0 9px 0 14px;
`),
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);
`, [
cM('disabled', {
color: 'var(--n-header-text-color-disabled)'
})
]),
cE('extra', `
transition: color .3s var(--n-bezier);
font-size: var(--n-extra-font-size);
justify-self: flex-end;
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;
position: relative;
display: flex;
flex-direction: column;
border-radius: inherit;
border-top-left-radius: 0;
border-top-right-radius: 0;
`, [
cB('transfer-filter', `
padding: 0 8px 8px 8px;
box-sizing: border-box;
background-color: var(--n-header-color);
transition:
border-color .3s var(--n-bezier),
background-color .3s var(--n-bezier);
border-bottom: 1px solid var(--n-filter-divider-color);
`),
cB('transfer-list-flex-container', `
flex: 1;
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%);
`, [
fadeInTransition()
]),
cB('transfer-list-content', `
padding: 0;
margin: 0;
position: relative;
`, [
cM('transition-disabled', [
cB('transfer-list-item', {
animation: 'none !important'
})
]),
cB('transfer-list-item', `
height: var(--n-item-height);
max-height: var(--n-item-height);
transition:
background-color .3s var(--n-bezier),
color .3s var(--n-bezier);
position: relative;
cursor: pointer;
display: flex;
align-items: center;
color: var(--n-item-text-color);
`, [
cNotM('disabled', [
c('&:hover', {
backgroundColor: 'var(--n-item-color-pending)'
})
]),
cE('extra', `
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding-right: 4px;
`),
cE('checkbox', `
display: flex;
align-items: center;
position: relative;
padding: 0 9px 0 14px;
`),
cM('disabled', `
cursor: not-allowed
background-color: #0000;
color: var(--n-item-text-color-disabled);
`),
cM('source', {
animationFillMode: 'forwards'
}, [
c('&.item-enter-active', `
transform: translateX(150%);
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;
`),
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;
`)
]),
cM('target', {
animationFillMode: 'forwards'
}, [
c('&.item-enter-active', `
transform: translateX(-150%);
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;
`),
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;
`)
])
])
])
])
])
]),
cB('transfer-gap', {
width: '72px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column'
}),
cB('button', [
c('&:first-child', {
marginBottom: '12px'
})
])
]),
animation
])

View File

@ -0,0 +1,171 @@
import { ref, computed, toRef, Ref } from 'vue'
import { useMemo, useMergedState } from 'vooks'
import type { Option, OptionValue, Filter, CheckedStatus } from './interface'
interface UseTransferDataProps {
defaultValue: OptionValue[] | null
value?: OptionValue[] | null
options: Option[]
filterable: boolean
filter: Filter
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function useTransferData (
props: UseTransferDataProps,
mergedDisabledRef: Ref<boolean>
) {
const uncontrolledValueRef = ref(props.defaultValue)
const controlledValueRef = toRef(props, 'value')
const mergedValueRef = useMergedState(
controlledValueRef,
uncontrolledValueRef
)
const optMapRef = computed(() => {
const map = new Map()
;(props.options || []).forEach((opt) => map.set(opt.value, opt))
return map
})
const tgtValueSetRef = computed(() => new Set(mergedValueRef.value || []))
const srcOptsRef = computed(() =>
props.options.filter((option) => !tgtValueSetRef.value.has(option.value))
)
const tgtOptsRef = computed(() => {
const optMap = optMapRef.value
return (mergedValueRef.value || []).map((v) => optMap.get(v))
})
const srcPatternRef = ref('')
const tgtPatternRef = ref('')
const filteredSrcOptsRef = computed(() => {
if (!props.filterable) return srcOptsRef.value
const { filter } = props
return srcOptsRef.value.filter((opt) =>
filter(srcPatternRef.value, opt, 'source')
)
})
const filteredTgtOptsRef = computed(() => {
if (!props.filterable) return tgtOptsRef.value
const { filter } = props
return tgtOptsRef.value.filter((opt) =>
filter(tgtPatternRef.value, opt, 'target')
)
})
const avlSrcValueSetRef = computed(
() =>
new Set(
filteredSrcOptsRef.value
.filter((opt) => !opt.disabled)
.map((opt) => opt.value)
)
)
const avlTgtValueSetRef = computed(
() =>
new Set(
filteredTgtOptsRef.value
.filter((opt) => !opt.disabled)
.map((opt) => opt.value)
)
)
const srcCheckedValuesRef = ref<OptionValue[]>([])
const tgtCheckedValuesRef = ref<OptionValue[]>([])
const srcCheckedStatusRef = computed<CheckedStatus>(() => {
const srcCheckedLength = srcCheckedValuesRef.value.filter((v) =>
avlSrcValueSetRef.value.has(v)
).length
const avlValueCount = avlSrcValueSetRef.value.size
if (avlValueCount === 0) {
return {
checked: false,
indeterminate: false,
disabled: true
}
} else if (srcCheckedLength === 0) {
return {
checked: false,
indeterminate: false
}
} else if (srcCheckedLength === avlValueCount) {
return {
checked: true,
indeterminate: false
}
} else {
return {
checked: false,
indeterminate: true
}
}
})
const tgtCheckedStatusRef = computed(() => {
const tgtCheckedLength = tgtCheckedValuesRef.value.filter((v) =>
avlTgtValueSetRef.value.has(v)
).length
const avlValueCount = avlTgtValueSetRef.value.size
if (avlValueCount === 0) {
return {
checked: false,
indeterminate: false,
disabled: true
}
} else if (tgtCheckedLength === 0) {
return {
checked: false,
indeterminate: false
}
} else if (tgtCheckedLength === avlValueCount) {
return {
checked: true,
indeterminate: false
}
} else {
return {
checked: false,
indeterminate: true
}
}
})
const fromButtonDisabledRef = useMemo(() => {
if (mergedDisabledRef.value) return true
return tgtCheckedValuesRef.value.length === 0
})
const toButtonDisabledRef = useMemo(() => {
if (mergedDisabledRef.value) return true
return srcCheckedValuesRef.value.length === 0
})
const isInputingRef = ref(false)
function handleInputFocus (): void {
isInputingRef.value = true
}
function handleInputBlur (): void {
isInputingRef.value = false
}
function handleSrcFilterUpdateValue (value: string | null): void {
srcPatternRef.value = value ?? ''
}
function handleTgtFilterUpdateValue (value: string | null): void {
tgtPatternRef.value = value ?? ''
}
return {
uncontrolledValue: uncontrolledValueRef,
mergedValue: mergedValueRef,
avlSrcValueSet: avlSrcValueSetRef,
avlTgtValueSet: avlTgtValueSetRef,
tgtOpts: tgtOptsRef,
srcOpts: srcOptsRef,
filteredSrcOpts: filteredSrcOptsRef,
filteredTgtOpts: filteredTgtOptsRef,
srcCheckedValues: srcCheckedValuesRef,
tgtCheckedValues: tgtCheckedValuesRef,
srcCheckedStatus: srcCheckedStatusRef,
tgtCheckedStatus: tgtCheckedStatusRef,
srcPattern: srcPatternRef,
tgtPattern: tgtPatternRef,
isInputing: isInputingRef,
fromButtonDisabled: fromButtonDisabledRef,
toButtonDisabled: toButtonDisabledRef,
handleInputFocus,
handleInputBlur,
handleTgtFilterUpdateValue,
handleSrcFilterUpdateValue
}
}

View File

@ -0,0 +1,4 @@
export default {
extraFontSize: '12px',
width: '440px'
}

View File

@ -0,0 +1,65 @@
import commonVariables from './_common'
import { checkboxDark } from '../../checkbox/styles'
import { scrollbarDark } from '../../_internal/scrollbar/styles'
import { inputDark } from '../../input/styles'
import { commonDark } from '../../_styles/common'
import { emptyDark } from '../../empty/styles'
import { buttonDark } from '../../button/styles'
import type { TransferTheme } from './light'
const transferDark: TransferTheme = {
name: 'Transfer',
common: commonDark,
peers: {
Checkbox: checkboxDark,
Scrollbar: scrollbarDark,
Input: inputDark,
Empty: emptyDark,
Button: buttonDark
},
self (vars) {
const {
iconColorDisabled,
iconColor,
fontWeight,
fontSizeLarge,
fontSizeMedium,
fontSizeSmall,
heightLarge,
heightMedium,
heightSmall,
borderRadius,
inputColor,
tableHeaderColor,
textColor1,
textColorDisabled,
textColor2,
hoverColor
} = vars
return {
...commonVariables,
itemHeightSmall: heightSmall,
itemHeightMedium: heightMedium,
itemHeightLarge: heightLarge,
fontSizeSmall,
fontSizeMedium,
fontSizeLarge,
borderRadius,
borderColor: '#0000',
listColor: inputColor,
headerColor: tableHeaderColor,
titleTextColor: textColor1,
titleTextColorDisabled: textColorDisabled,
extraTextColor: textColor2,
filterDividerColor: '#0000',
itemTextColor: textColor2,
itemTextColorDisabled: textColorDisabled,
itemColorPending: hoverColor,
titleFontWeight: fontWeight,
iconColor,
iconColorDisabled
}
}
}
export default transferDark

View File

@ -0,0 +1,3 @@
export { default as transferDark } from './dark'
export { default as transferLight } from './light'
export type { TransferTheme, TransferThemeVars } from './light'

View File

@ -0,0 +1,73 @@
import commonVariables from './_common'
import { composite } from 'seemly'
import { checkboxLight } from '../../checkbox/styles'
import { scrollbarLight } from '../../_internal/scrollbar/styles'
import { inputLight } from '../../input/styles'
import { commonLight } from '../../_styles/common'
import type { ThemeCommonVars } from '../../_styles/common'
import { emptyLight } from '../../empty/styles'
import { buttonLight } from '../../button/styles'
import { createTheme } from '../../_mixins'
const self = (vars: ThemeCommonVars) => {
const {
fontWeight,
iconColorDisabled,
iconColor,
fontSizeLarge,
fontSizeMedium,
fontSizeSmall,
heightLarge,
heightMedium,
heightSmall,
borderRadius,
cardColor,
tableHeaderColor,
textColor1,
textColorDisabled,
textColor2,
borderColor,
hoverColor
} = vars
return {
...commonVariables,
itemHeightSmall: heightSmall,
itemHeightMedium: heightMedium,
itemHeightLarge: heightLarge,
fontSizeSmall,
fontSizeMedium,
fontSizeLarge,
borderRadius,
borderColor,
listColor: cardColor,
headerColor: composite(cardColor, tableHeaderColor),
titleTextColor: textColor1,
titleTextColorDisabled: textColorDisabled,
extraTextColor: textColor2,
filterDividerColor: borderColor,
itemTextColor: textColor2,
itemTextColorDisabled: textColorDisabled,
itemColorPending: hoverColor,
titleFontWeight: fontWeight,
iconColor,
iconColorDisabled
}
}
export type TransferThemeVars = ReturnType<typeof self>
const transferLight = createTheme({
name: 'Transfer',
common: commonLight,
peers: {
Checkbox: checkboxLight,
Scrollbar: scrollbarLight,
Input: inputLight,
Empty: emptyLight,
Button: buttonLight
},
self
})
export default transferLight
export type TransferTheme = typeof transferLight

View File

@ -0,0 +1,79 @@
import { mount } from '@vue/test-utils'
import { sleep } from 'seemly'
import { NTransfer } from '../index'
describe('n-transfer', () => {
it('should work with import on demand', () => {
mount(NTransfer)
})
it('should work with `disabled` prop', () => {
const wrapper = mount(NTransfer, { props: { disabled: true } })
expect(wrapper.find('.n-transfer').attributes('class')).toContain(
'n-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'
)
})
it('should work with `filter` prop', async () => {
const options = [
{
label: 'test1',
value: 'test1'
}
]
const onFilter = jest.fn()
const wrapper = mount(NTransfer, {
props: { filterable: true, filter: onFilter, options: options }
})
await wrapper.find('input').setValue('1')
await sleep(300)
expect(onFilter).toHaveBeenCalled()
})
it('should work with `size` prop', async () => {
;(['small', 'medium', 'large'] as const).forEach((i) => {
const wrapper = mount(NTransfer, {
props: { size: i }
})
expect(wrapper.find('.n-transfer').attributes('style')).toMatchSnapshot()
})
})
it('should work with `source-filter-placeholder`、`target-filter-placeholder` props', async () => {
const wrapper = mount(NTransfer, {
props: {
filterable: true,
'source-filter-placeholder': 'test-source',
'target-filter-placeholder': 'test-target'
}
})
expect(wrapper.findAll('input')[0].attributes('placeholder')).toBe(
'test-source'
)
expect(wrapper.findAll('input')[1].attributes('placeholder')).toBe(
'test-target'
)
})
it('should work with `source-title`、`target-title` props', async () => {
const wrapper = mount(NTransfer, {
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'
)
})
})

View File

@ -0,0 +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-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);"`;

View File

@ -0,0 +1,19 @@
/**
* @jest-environment node
*/
import { h, createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'
import { setup } from '@css-render/vue3-ssr'
import { NTransfer } from '../..'
describe('SSR', () => {
it('works', async () => {
const app = createSSRApp(() => <NTransfer />)
setup(app)
try {
await renderToString(app)
} catch (e) {
expect(e).not.toBeTruthy()
}
})
})