perf(transfer): use many tricks to improve performance

This commit is contained in:
07akioni 2020-01-18 13:28:20 +08:00
parent 3a35c4eaff
commit 4deba40870
15 changed files with 791 additions and 301 deletions

View File

@ -11,23 +11,24 @@
<n-button @click="regenValues">
Regen Values
</n-button>
<pre class="n-doc-section__inspect">{{ JSON.stringify(value) }}</pre>
<pre class="n-doc-section__inspect">{{ $refs.transfer ? $refs.transfer._data : null }}</pre>
<!-- <pre class="n-doc-section__inspect">{{ JSON.stringify(value) }}</pre>
<pre class="n-doc-section__inspect">{{ $refs.transfer ? $refs.transfer.memorizedSourceOptions.map(option => option.value) : null }}</pre>
<pre class="n-doc-section__inspect">{{ $refs.transfer ? $refs.transfer.targetOptions.map(option => option.value) : null }}</pre> -->
```
```js
let prefix = null
function genOptions () {
prefix = Math.random().toString(36).slice(2, 5)
return Array.apply(null, { length: 20 }).map((v, i) => ({
return Array.apply(null, { length: 1000 }).map((v, i) => ({
label: prefix + 'Option' + i,
value: prefix + i,
disabled: i % 3 === 0
disabled: i % 5 === 0
}))
}
function genValues () {
return Array.apply(null, { length: 5 }).map((v, i) => prefix + i)
return Array.apply(null, { length: 500 }).map((v, i) => prefix + i)
}
export default {

View File

@ -58,7 +58,7 @@ export default {
],
model: {
prop: 'checked',
event: 'input'
event: 'change'
},
props: {
value: {
@ -103,8 +103,7 @@ export default {
if (this.NCheckboxGroup) {
this.NCheckboxGroup.toggleCheckbox(!this.synthesizedChecked, this.value)
} else {
this.$emit('input', !this.synthesizedChecked)
this.$emit('change', !this.synthesizedChecked, this.value)
this.$emit('change', !this.synthesizedChecked, this.synthesizedChecked)
}
},
handleClick (e) {

View File

@ -0,0 +1,85 @@
<template>
<div
class="n-checkbox"
:class="{
'n-checkbox--checked': checked,
'n-checkbox--disabled': disabled,
'n-checkbox--indeterminate': indeterminate,
[`n-${theme}-theme`]: theme
}"
:tabindex="disabled ? false : 0"
@keyup.enter="handleKeyUpEnter"
@keyup.space="handleKeyUpSpace"
@keydown.space="handleKeyDownSpace"
>
<div
class="n-checkbox-box"
@click="handleClick"
>
<check-mark class="n-checkbox-box__check-mark" />
<line-mark class="n-checkbox-box__line-mark" />
</div>
</div>
</template>
<script>
import CheckMark from './CheckMark'
import LineMark from './LineMark'
import createValidator from '../../../utils/validateProp'
export default {
name: 'NSimpleCheckbox',
components: {
CheckMark,
LineMark
},
model: {
prop: 'checked',
event: 'chanage'
},
props: {
value: {
validator: createValidator(['number', 'boolean', 'string']),
default: null
},
checked: {
validator: createValidator(['boolean']),
default: false
},
disabled: {
validator: createValidator(['boolean']),
default: false
},
indeterminate: {
validator: createValidator(['boolean']),
default: false
},
theme: {
validator: createValidator(['string']),
default: null
}
},
methods: {
handleClick (e) {
this.$emit('click', e)
if (!this.disabled) {
this.toggle()
}
},
handleKeyUpEnter (e) {
if (!this.disabled) {
this.toggle()
}
},
toggle () {
this.$emit('change', !this.checked, this.checked)
},
handleKeyDownSpace (e) {
e.preventDefault()
},
handleKeyUpSpace (e) {
this.handleKeyUpEnter()
}
}
}
</script>

View File

@ -1,7 +1,9 @@
/* istanbul ignore file */
import Transfer from './src/Transfer.vue'
import installPropsUnsafeTransition from '../../utils/installPropsUnsafeTransition'
Transfer.install = function (Vue) {
installPropsUnsafeTransition(Vue)
Vue.component(Transfer.name, Transfer)
}

View File

@ -8,68 +8,44 @@
<div class="n-transfer-list">
<div class="n-transfer-list-header">
<div class="n-transfer-list-header__checkbox">
<div class="n-transfer-list-light-bar" />
<n-checkbox
:checked="sourceHeaderCheckboxChecked"
:indeterminate="sourceHeaderCheckboxIndeterminate"
:disabled="sourceHeaderCheckboxDisabled"
@input="handleSourceHeaderCheckboxInput"
/>
<n-transfer-header-checkbox :source="true" :theme="synthesizedTheme" @change="handleSourceHeaderCheckboxChange" />
</div>
<div class="n-transfer-list-header__header">
Source
</div>
<div class="n-transfer-list-header__extra">
{{ sourceCheckedValueSet.size }} / {{ sourceOptions.length }}
</div>
<n-transfer-header-extra :source="true" />
</div>
<div
class="n-transfer-list-body"
@mouseleave="handleSourceListMouseLeave"
>
<n-scrollbar ref="leftScrollbar">
<ul class="n-transfer-list-content">
<transition name="n-transfer-list-light-bar--transition">
<div
v-if="showLightBar"
class="n-transfer-list-light-bar"
:style="{
top: lightBarStyleTop
}"
/>
</transition>
<n-transfer-list-item
v-for="option in memorizedSourceOptions"
<n-scrollbar @scroll="handleSourceListScroll">
<ul ref="sourceList" class="n-transfer-list-content n-transfer-list-content--animation-disabled">
<n-transfer-light-bar ref="lightBar" />
<n-transfer-source-list-item
v-for="(option, index) in memorizedSourceOptions"
ref="sourceListItems"
:key="option.value"
:show="sourceValueSet.has(option.value)"
:index="index"
:value="option.value"
:checked="sourceCheckedValueSet.has(option.value)"
:disabled="option.disabled"
source
:title="option.label"
@click="handleSourceCheckboxInput(
!sourceCheckedValueSet.has(option.value),
option.value
)"
:disabled="!!option.disabled"
:label="option.label"
@click="handleSourceCheckboxClick"
@mouseenter="handleSourceOptionMouseEnter"
@mouseleave="handleSourceOptionMouseLeave"
>
{{ option.label }}
</n-transfer-list-item>
/>
</ul>
</n-scrollbar>
</div>
</div>
<div class="n-transfer-gap">
<n-transfer-button
to
:disabled="toTargetButtonDisabled"
:to="true"
@click="handleToTargetClick"
>
To Target
</n-transfer-button>
<n-transfer-button
:disabled="toSourceButtonDisabled"
@click="handleToSourceClick"
>
To Source
@ -78,53 +54,32 @@
<div class="n-transfer-list">
<div class="n-transfer-list-header">
<div class="n-transfer-list-header__checkbox">
<n-checkbox
:checked="targetHeaderCheckboxChecked"
:indeterminate="targetHeaderCheckboxIndeterminate"
:disabled="targetHeaderCheckboxDisabled"
@input="handleTargetHeaderCheckboxInput"
/>
<n-transfer-header-checkbox :theme="synthesizedTheme" @change="handleTargetHeaderCheckboxChange" />
</div>
<div class="n-transfer-list-header__header">
Target
</div>
<div class="n-transfer-list-header__extra">
{{ targetCheckedValueSet.size }} / {{ targetOptions.length }}
</div>
<n-transfer-header-extra />
</div>
<div
class="n-transfer-list-body"
@mouseleave="handleTargetListMouseLeave"
>
<n-scrollbar ref="rightScrollbar">
<ul class="n-transfer-list-content">
<transition name="n-transfer-list-light-bar--transition">
<div
v-if="showSecondLightBar"
class="n-transfer-list-light-bar"
:style="{
top: secondLightBarStyleTop
}"
/>
</transition>
<n-transfer-list-item
v-for="option in memorizedTargetOptions"
<n-scrollbar ref="rightScrollbar" @scroll="handleTargetListScroll">
<ul ref="targetList" class="n-transfer-list-content n-transfer-list-content--animation-disabled">
<n-transfer-light-bar ref="secondLightBar" />
<n-transfer-target-list-item
v-for="(option, index) in targetOptions"
ref="targetListItems"
:key="option.value"
:show="targetValueSet.has(option.value)"
:index="index"
:value="option.value"
:checked="targetCheckedValueSet.has(option.value)"
:disabled="option.disabled"
target
:title="option.label"
@click="handleTargetCheckboxInput(
!targetCheckedValueSet.has(option.value),
option.value
)"
:disabled="!!option.disabled"
:label="option.label"
@click="handleTargetCheckboxClick"
@mouseenter="handleTargetOptionMouseEnter"
@mouseleave="handleTargetOptionMouseLeave"
>
{{ option.label }}
</n-transfer-list-item>
/>
</ul>
</n-scrollbar>
</div>
@ -133,26 +88,37 @@
</template>
<script>
import NCheckbox from '../../Checkbox'
import NScrollbar from '../../Scrollbar'
import NTransferListItem from './TransferListItem'
import NTransferHeaderCheckbox from './TransferHeaderCheckbox'
import NTransferHeaderExtra from './TransferHeaderExtra'
import NTransferSourceListItem from './TransferSourceListItem'
import NTransferTargetListItem from './TransferTargetListItem'
import NTransferButton from './TransferButton'
import cloneDeep from 'lodash/cloneDeep'
import withlightbar from '../../../mixins/withlightbar'
import withsecondlightbar from '../../../mixins/withsecondlightbar'
import asformitem from '../../../mixins/asformitem'
import withapp from '../../../mixins/withapp'
import themeable from '../../../mixins/themeable'
import NTransferLightBar from './TransferLightBar'
import debounce from 'lodash-es/debounce'
const SCROLL_VISIBLE_BUFFER = 1200
export default {
name: 'NTransfer',
components: {
NCheckbox,
NTransferHeaderCheckbox,
NTransferHeaderExtra,
NScrollbar,
NTransferListItem,
NTransferButton
NTransferSourceListItem,
NTransferTargetListItem,
NTransferButton,
NTransferLightBar
},
mixins: [withapp, themeable, asformitem()],
model: {
prop: 'value',
event: 'change'
},
mixins: [withapp, themeable, asformitem(), withlightbar, withsecondlightbar],
props: {
value: {
type: Array,
@ -167,204 +133,280 @@ export default {
default: false
}
},
provide () {
return {
NTransfer: this
}
},
data () {
return {
sourceCheckedValues: [],
targetCheckedValues: [],
memorizedSourceOptions: null,
memorizedTargetOptions: null,
init: true,
active: true
sourceListVisibleMinIndex: -SCROLL_VISIBLE_BUFFER / 34,
sourceListVisibleMaxIndex: SCROLL_VISIBLE_BUFFER / 34,
targetListVisibleMinIndex: -SCROLL_VISIBLE_BUFFER / 34,
targetListVisibleMaxIndex: SCROLL_VISIBLE_BUFFER / 34,
initialized: false
}
},
computed: {
toTargetButtonDisabled () {
return this.disabled || this.sourceCheckedValueSet.size === 0
},
toSourceButtonDisabled () {
return this.disabled || this.targetCheckedValueSet.size === 0
},
sourceEnabledOptions () {
return this.sourceOptions.filter(option => !option.disabled)
},
targetEnabledOptions () {
return this.targetOptions.filter(option => !option.disabled)
mergedDisabledStatus () {
return {
source: this.memorizedSourceOptions.every(option => option.disabled),
target: this.targetOptions.every(option => option.disabled)
}
},
sourceHeaderCheckboxDisabled () {
return this.disabled || !this.sourceEnabledOptions.length
return this.disabled || this.mergedDisabledStatus.source
},
targetHeaderCheckboxDisabled () {
return this.disabled || !this.targetEnabledOptions.length
return this.disabled || this.mergedDisabledStatus.target
},
sourceHeaderCheckboxChecked () {
return this.sourceCheckedValueSet.size === this.sourceOptions.length && !!this.sourceOptions.length
return this.sourceCheckedValues.length === this.memorizedSourceOptions.length && !!this.memorizedSourceOptions.length
},
targetHeaderCheckboxChecked () {
return this.targetCheckedValueSet.size === this.targetOptions.length && !!this.targetOptions.length
return this.targetCheckedValues.length === this.targetOptions.length && !!this.targetOptions.length
},
valueToOptionMap () {
const map = new Map()
this.options.forEach(option => {
map.set(option.value, option)
map.set(option.value, { ...option })
})
return map
},
sourceHeaderCheckboxIndeterminate () {
return this.sourceCheckedValueSet.size !== 0 && this.sourceCheckedValueSet.size < this.sourceOptions.length
return this.sourceCheckedValues.length !== 0 && this.sourceCheckedValues.length < this.memorizedSourceOptions.length
},
targetHeaderCheckboxIndeterminate () {
return this.targetCheckedValueSet.size !== 0 && this.targetCheckedValueSet.size < this.targetOptions.length
},
sourceValueSet () {
return new Set(this.sourceOptions.map(option => option.value))
},
targetValueSet () {
return new Set(this.targetOptions.map(option => option.value))
return this.targetCheckedValues.length !== 0 && this.targetCheckedValues.length < this.targetOptions.length
},
sourceCheckedValueSet () {
return new Set(this.sourceCheckedValues.filter(value => this.valueToOptionMap.has(value)))
const valueToOptionMap = this.valueToOptionMap
return new Set(this.sourceCheckedValues.filter(value => valueToOptionMap.has(value)))
},
targetCheckedValueSet () {
return new Set(this.targetCheckedValues.filter(value => this.valueToOptionMap.has(value)))
const valueToOptionMap = this.valueToOptionMap
return new Set(this.targetCheckedValues.filter(value => valueToOptionMap.has(value)))
},
sourceOptions () {
const valueSet = Array.isArray(this.value) ? new Set(this.value) : new Set()
return this.options.filter(option => !valueSet.has(option.value))
valueSet () {
return Array.isArray(this.value) ? new Set(this.value) : new Set()
},
sourceValueSet () {
return this.mergedValueSet.sourceValueSet
},
targetValueSet () {
return this.mergedValueSet.targetValueSet
},
targetOptionsWithShowStatus () {
return this.targetOptions.map((option, index) => {
const show = true
option.show = show
option.index = index
return option
})
},
mergedValueSet () {
const valueSet = this.valueSet
const sourceValueSet = new Set()
const targetValueSet = new Set()
this.options.forEach(option => {
if (valueSet.has(option.value)) {
targetValueSet.add(option.value)
} else {
sourceValueSet.add(option.value)
}
})
return {
sourceValueSet,
targetValueSet
}
},
targetOptions () {
const valueSet = Array.isArray(this.value) ? new Set(this.value) : new Set()
return this.options.filter(option => valueSet.has(option.value))
},
orderedOptions () {
return this.sourceOptions.concat(this.targetOptions)
const vModelValue = Array.isArray(this.value) ? this.value : []
const valueMap = this.valueToOptionMap
const targetOptions = []
vModelValue.forEach(value => {
const option = valueMap.get(value)
if (option !== undefined) targetOptions.push(option)
})
return targetOptions
}
},
watch: {
options (newOptions) {
this.init = true
this.initialized = false
const valueSet = this.valueSet
this.memorizedSourceOptions = cloneDeep(this.options.filter(option => !valueSet.has(option.value)))
this.sourceCheckedValues = []
this.targetCheckedValues = []
this.$nextTick().then(() => {
this.memorizedSourceOptions = cloneDeep(newOptions)
this.memorizedTargetOptions = cloneDeep(newOptions)
this.sourceCheckedValues = []
this.targetCheckedValues = []
return this.$nextTick()
}).then(() => {
this.init = false
this.initialized = true
})
},
value (value, oldValue) {
this.$emit('change', value, oldValue)
}
},
mounted () {
this.$nextTick().then(() => {
this.init = false
})
this.initialized = true
},
created () {
this.memorizedSourceOptions = cloneDeep(this.options)
this.memorizedTargetOptions = cloneDeep(this.options)
const valueSet = this.valueSet
this.memorizedSourceOptions = cloneDeep(this.options.filter(option => !valueSet.has(option.value)))
},
methods: {
emitInputEvent (value) {
handleSourceListScroll: debounce(function (_, container) {
const scrollTop = container.scrollTop
this.sourceListVisibleMinIndex = (scrollTop - SCROLL_VISIBLE_BUFFER) / 34
this.sourceListVisibleMaxIndex = (scrollTop + SCROLL_VISIBLE_BUFFER) / 34
}, 128),
handleTargetListScroll: debounce(function (_, container) {
const scrollTop = container.scrollTop
this.targetListVisibleMinIndex = (scrollTop - SCROLL_VISIBLE_BUFFER) / 34
this.targetListVisibleMaxIndex = (scrollTop + SCROLL_VISIBLE_BUFFER) / 34
}, 128),
emitChangeEvent (value) {
const newValue = this.cleanValue(value)
this.$emit('input', newValue)
this.$emit('change', newValue)
},
cleanValue (value) {
if (Array.isArray(value)) {
return value.filter((v) => this.valueToOptionMap.has(v))
} else return null
},
handleSourceHeaderCheckboxInput (value) {
handleSourceHeaderCheckboxChange (value) {
if (this.sourceHeaderCheckboxIndeterminate) {
(this.$refs.sourceListItems || []).forEach(listItem => listItem.setChecked(false))
this.sourceCheckedValues = []
return
}
if (value) {
const newValues = this.sourceOptions.filter(option => !option.disabled).map(option => option.value).concat(this.sourceCheckedValues)
(this.$refs.sourceListItems || []).forEach(listItem => listItem.setChecked(true))
const newValues = this.memorizedSourceOptions.filter(option => !option.disabled).map(option => option.value).concat(this.sourceCheckedValues)
this.sourceCheckedValues = Array.from(new Set(newValues))
} else {
(this.$refs.sourceListItems || []).forEach(listItem => listItem.setChecked(false))
this.sourceCheckedValues = []
}
},
handleTargetHeaderCheckboxInput (value) {
handleTargetHeaderCheckboxChange (value) {
if (this.targetHeaderCheckboxIndeterminate) {
(this.$refs.targetListItems || []).forEach(listItem => listItem.setChecked(false))
this.targetCheckedValues = []
return
}
if (value) {
(this.$refs.targetListItems || []).forEach(listItem => listItem.setChecked(true))
const newValues = this.targetOptions.filter(option => !option.disabled).map(option => option.value).concat(this.targetCheckedValues)
this.targetCheckedValues = Array.from(new Set(newValues))
} else {
(this.$refs.targetListItems || []).forEach(listItem => listItem.setChecked(false))
this.targetCheckedValues = []
}
},
handleTargetCheckboxInput (checked, value) {
handleTargetCheckboxClick (checked, optionValue) {
if (checked) {
this.targetCheckedValues.push(value)
this.targetCheckedValues.push(optionValue)
} else {
const index = this.targetCheckedValues.findIndex(v => v === value)
if (~index) this.targetCheckedValues.splice(index, 1)
const index = this.targetCheckedValues.findIndex(v => v === optionValue)
if (~index) {
this.targetCheckedValues.splice(index, 1)
}
}
},
handleSourceCheckboxInput (checked, value) {
handleSourceCheckboxClick (checked, optionValue) {
if (checked) {
this.sourceCheckedValues.push(value)
this.sourceCheckedValues.push(optionValue)
} else {
const index = this.sourceCheckedValues.findIndex(v => v === value)
if (~index) this.sourceCheckedValues.splice(index, 1)
const index = this.sourceCheckedValues.findIndex(v => v === optionValue)
if (~index) {
this.sourceCheckedValues.splice(index, 1)
}
}
},
handleToTargetClick () {
const enteredItemEls = Array.from(this.$el.getElementsByClassName('n-transfer-list-item--enter'))
const length = enteredItemEls.length
for (let i = 0; i < length; ++i) {
enteredItemEls[i].classList.remove('n-transfer-list-item--enter')
}
this.$refs.sourceList.classList.remove('n-transfer-list-content--animation-disabled')
this.$refs.targetList.classList.remove('n-transfer-list-content--animation-disabled')
const sourceCheckedValues = this.sourceCheckedValues
/** create new value */
let newValue = Array.isArray(this.value) ? this.value : []
newValue = this.sourceCheckedValues.concat(newValue)
const headTargetOptions = this.sourceCheckedValues.map(value => this.valueToOptionMap.get(value)).map(option => ({
...option
}))
const tailTargetOptions = this.memorizedTargetOptions.filter(option => !this.sourceCheckedValueSet.has(option.value)).map(option => ({
...option
}))
const reorderedTargetOptions = headTargetOptions.concat(tailTargetOptions)
this.memorizedTargetOptions = reorderedTargetOptions
this.$nextTick().then(() => {
this.emitInputEvent(newValue)
newValue = sourceCheckedValues.concat(newValue)
/** play source leave animation */
const sourceCheckedValueSet = this.sourceCheckedValueSet
this.$refs.sourceListItems.forEach(listItem => {
if (sourceCheckedValueSet.has(listItem.value)) {
listItem.leave()
}
})
window.setTimeout(() => {
/** disable animation before apply dom change */
this.$refs.sourceList.classList.add('n-transfer-list-content--animation-disabled')
/** after animation is done change memorized source options to remove dom */
this.memorizedSourceOptions = this.memorizedSourceOptions.filter(option => !sourceCheckedValueSet.has(option.value))
}, 300)
/** clear check */
;(this.$refs.sourceListItems || []).forEach(listItem => listItem.setChecked(false))
this.sourceCheckedValues = []
/** emit new value */
/** auto play target options enter animation */
this.$emit('change', newValue)
},
handleToSourceClick () {
const enteredItemEls = Array.from(this.$el.getElementsByClassName('n-transfer-list-item--enter'))
const length = enteredItemEls.length
for (let i = 0; i < length; ++i) {
enteredItemEls[i].classList.remove('n-transfer-list-item--enter')
}
this.$refs.sourceList.classList.remove('n-transfer-list-content--animation-disabled')
this.$refs.targetList.classList.remove('n-transfer-list-content--animation-disabled')
/** create new value */
let newValue = Array.isArray(this.value) ? this.value : []
newValue = newValue.filter(value => !this.targetCheckedValueSet.has(value))
const headSourceOptions = this.targetCheckedValues.map(value => this.valueToOptionMap.get(value)).map(option => ({
...option
}))
const tailSourceOptions = this.memorizedSourceOptions.filter(option => !this.targetCheckedValueSet.has(option.value)).map(option => ({
...option
}))
const reorderedSourceOptions = headSourceOptions.concat(tailSourceOptions)
this.memorizedSourceOptions = reorderedSourceOptions
this.$nextTick().then(() => {
this.emitInputEvent(newValue)
const targetValueSet = this.targetCheckedValueSet
/** play target leave animation */
this.$refs.targetListItems.forEach(listItem => {
if (targetValueSet.has(listItem.value)) {
listItem.leave()
}
})
window.setTimeout(() => {
this.$refs.targetList.classList.add('n-transfer-list-content--animation-disabled')
/** disable animation before apply dom change */
this.targetAnimationDisabled = true
/** after animation is done change value to remove dom */
newValue = newValue.filter(value => !targetValueSet.has(value))
/** emit new value */
this.emitChangeEvent(newValue)
}, 300)
/** change memorized source options */
const valueToOptionMap = this.valueToOptionMap
const newSourceOptions = this.targetCheckedValues.map(value => valueToOptionMap.get(value))
this.memorizedSourceOptions = newSourceOptions.concat(this.memorizedSourceOptions)
/** clear check */
;(this.$refs.targetListItems || []).forEach(listItem => listItem.setChecked(false))
this.targetCheckedValues = []
},
handleSourceOptionMouseEnter (e) {
this.updateLightBarPosition(e.target)
},
handleTargetOptionMouseEnter (e) {
this.updateSecondLightBarPosition(e.target)
},
handleSourceOptionMouseLeave (e) {
this.hideLightBar()
},
handleTargetOptionMouseLeave (e) {
this.hideSecondLightBar()
},
handleSourceListMouseLeave () {
this.hideLightBar()
},
handleTargetListMouseLeave () {
this.hideSecondLightBar()
}
handleSourceOptionMouseEnter: debounce(function (e) {
this.$refs.lightBar.updateLightBarPosition(e.target)
}, 128),
handleTargetOptionMouseEnter: debounce(function (e) {
this.$refs.secondLightBar.updateLightBarPosition(e.target)
}, 128),
handleSourceOptionMouseLeave: debounce(function (e) {
this.$refs.lightBar.hideLightBar()
}, 128),
handleTargetOptionMouseLeave: debounce(function (e) {
this.$refs.secondLightBar.hideLightBar()
}, 128),
handleSourceListMouseLeave: debounce(function () {
this.$refs.lightBar.hideLightBar()
}, 128),
handleTargetListMouseLeave: debounce(function () {
this.$refs.secondLightBar.hideLightBar()
}, 128)
}
}
</script>

View File

@ -21,17 +21,27 @@
</template>
<script>
import createValidator from '../../../utils/validateProp'
export default {
props: {
to: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
validator: createValidator(['boolean']),
default: false
}
},
inject: {
NTransfer: {
default: null
}
},
computed: {
disabled () {
if (this.NTransfer.disabled) return true
if (this.to) return this.NTransfer.sourceCheckedValues.length === 0
else return this.NTransfer.targetCheckedValues.length === 0
}
},
methods: {
handleClick () {
this.$emit('click')

View File

@ -0,0 +1,55 @@
<template>
<n-simple-checkbox
:theme="theme"
:checked="checked"
:indeterminate="indeterminate"
:disabled="disabled"
@change="handleChange"
/>
</template>
<script>
import NSimpleCheckbox from '../../Checkbox/src/SimpleCheckbox'
import createValidator from '../../../utils/validateProp'
export default {
name: 'NTransferHeaderCheckbox',
components: {
NSimpleCheckbox
},
props: {
theme: {
validator: createValidator(['string']),
default: null
},
source: {
validator: createValidator(['boolean']),
default: false
}
},
inject: {
NTransfer: {
default: null
}
},
computed: {
checked () {
if (this.source) return this.NTransfer.sourceHeaderCheckboxChecked
return this.NTransfer.targetHeaderCheckboxChecked
},
disabled () {
if (this.source) return this.NTransfer.sourceHeaderCheckboxDisabled
return this.NTransfer.targetHeaderCheckboxDisabled
},
indeterminate () {
if (this.source) return this.NTransfer.sourceHeaderCheckboxIndeterminate
return this.NTransfer.targetHeaderCheckboxIndeterminate
}
},
methods: {
handleChange (value) {
this.$emit('change', value)
}
}
}
</script>

View File

@ -0,0 +1,32 @@
<template>
<div class="n-transfer-list-header__extra">
{{
source ?
NTransfer.sourceCheckedValues.length :
NTransfer.targetCheckedValues.length
}} / {{
source ?
NTransfer.memorizedSourceOptions.length :
NTransfer.targetOptions.length
}}
</div>
</template>
<script>
import createValidator from '../../../utils/validateProp'
export default {
name: 'NTransferHeaderExtra',
props: {
source: {
validator: createValidator(['boolean']),
default: false
}
},
inject: {
NTransfer: {
default: null
}
}
}
</script>

View File

@ -0,0 +1,39 @@
<template>
<!-- <div style="position: absolute; left: 0; right: 0; top: 0; bottom: 0;"> -->
<transition name="n-transfer-list-light-bar--transition">
<div
v-show="show"
class="n-transfer-list-light-bar"
:style="{
top: styleTop
}"
/>
</transition>
<!-- </div> -->
</template>
<script>
export default {
data () {
return {
show: false,
styleTop: null,
vanishTimerId: null
}
},
methods: {
hideLightBar (delay = 300) {
this.vanishTimerId && window.clearTimeout(this.vanishTimerId)
this.vanishTimerId = window.setTimeout(() => {
this.show = false
}, delay)
},
updateLightBarPosition (el) {
this.vanishTimerId && window.clearTimeout(this.vanishTimerId)
this.vanishTimerId = null
this.show = true
this.styleTop = el.offsetTop + 'px'
}
}
}
</script>

View File

@ -1,100 +0,0 @@
<template>
<transition
:name="transitionName"
>
<li
v-show="show"
class="n-transfer-list-item"
:class="{
'n-transfer-list-item--disabled': disabled
}"
@click="handleClick"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<div class="n-transfer-list-item__checkbox">
<n-checkbox
:disabled="disabled"
:checked="checked"
@input="handleInput"
/>
</div>
<div
class="n-transfer-list-item__label"
:title="title"
>
<slot />
</div>
</li>
</transition>
</template>
<script>
import NCheckbox from '../../Checkbox'
export default {
name: 'NTransferListItem',
components: {
NCheckbox
},
props: {
checked: {
type: Boolean,
required: true
},
value: {
validator () {
return true
},
required: true
},
disabled: {
type: Boolean,
default: false
},
show: {
type: Boolean,
default: true
},
source: {
type: Boolean,
default: false
},
target: {
type: Boolean,
default: false
},
title: {
type: String,
required: true
}
},
computed: {
transitionName () {
return this.source ? 'n-transfer-list-item-source--transition' : 'n-transfer-list-item-target--transition'
}
},
methods: {
handleClick () {
if (!this.disabled) {
this.$emit('click')
}
},
handleInput (checked) {
if (!this.disabled) {
this.$emit('input', checked, this.value)
}
},
handleMouseEnter (e) {
if (!this.disabled) {
this.$emit('mouseenter', e)
}
},
handleMouseLeave (e) {
if (!this.disabled) {
this.$emit('mouseleave', e)
}
}
}
}
</script>

View File

@ -0,0 +1,104 @@
<template>
<li
class="n-transfer-list-item n-transfer-list-item--source"
:class="{
'n-transfer-list-item--disabled': disabled,
'n-transfer-list-item--enter': enableEnterAnimation
}"
@click="handleClick"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<div v-if="visible" class="n-transfer-list-item__checkbox">
<n-simple-checkbox
:theme="NTransfer.synthesizedTheme"
:disabled="disabled"
:checked="checked"
/>
</div>
<div
v-if="visible"
class="n-transfer-list-item__label"
>
{{ label }}
</div>
</li>
</template>
<script>
import NSimpleCheckbox from '../../Checkbox/src/SimpleCheckbox'
import createValidator from '../../../utils/validateProp'
export default {
name: 'NTransferListItem',
components: {
NSimpleCheckbox
},
props: {
label: {
validator: createValidator(['string']),
required: true
},
value: {
validator: createValidator(['string', 'number']),
required: true
},
disabled: {
validator: createValidator(['boolean']),
default: false
},
index: {
validator: createValidator(['number']),
required: true
}
},
inject: {
NTransfer: {
default: null
}
},
data () {
return {
checked: false,
enableEnterAnimation: false
}
},
computed: {
visible () {
return this.NTransfer.sourceListVisibleMinIndex < this.index && this.index < this.NTransfer.sourceListVisibleMaxIndex
}
},
created () {
if (this.NTransfer.initialized) {
this.enableEnterAnimation = true
}
},
methods: {
setChecked (checked) {
if (!this.disabled && this.checked !== checked) {
this.checked = checked
}
},
handleClick () {
if (!this.disabled) {
const newCheckedStatus = !this.checked
this.checked = newCheckedStatus
this.$emit('click', newCheckedStatus, this.value)
}
},
handleMouseEnter (e) {
if (!this.disabled) {
this.$emit('mouseenter', e)
}
},
handleMouseLeave (e) {
if (!this.disabled) {
this.$emit('mouseleave', e)
}
},
leave () {
this.$el.classList.add('n-transfer-list-item--leave')
}
}
}
</script>

View File

@ -0,0 +1,104 @@
<template>
<li
class="n-transfer-list-item n-transfer-list-item--target"
:class="{
'n-transfer-list-item--disabled': disabled,
'n-transfer-list-item--enter': enableEnterAnimation
}"
@click="handleClick"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<div v-if="visible" class="n-transfer-list-item__checkbox">
<n-simple-checkbox
:theme="NTransfer.synthesizedTheme"
:disabled="disabled"
:checked="checked"
/>
</div>
<div
v-if="visible"
class="n-transfer-list-item__label"
>
{{ label }}
</div>
</li>
</template>
<script>
import NSimpleCheckbox from '../../Checkbox/src/SimpleCheckbox'
import createValidator from '../../../utils/validateProp'
export default {
name: 'NTransferListItem',
components: {
NSimpleCheckbox
},
props: {
label: {
validator: createValidator(['string']),
required: true
},
value: {
validator: createValidator(['string', 'number']),
required: true
},
disabled: {
validator: createValidator(['boolean']),
required: true
},
index: {
validator: createValidator(['number']),
required: true
}
},
inject: {
NTransfer: {
default: null
}
},
data () {
return {
checked: false,
enableEnterAnimation: false
}
},
computed: {
visible () {
return this.NTransfer.targetListVisibleMinIndex < this.index && this.index < this.NTransfer.targetListVisibleMaxIndex
}
},
created () {
if (this.NTransfer.initialized) {
this.enableEnterAnimation = true
}
},
methods: {
setChecked (checked) {
if (!this.disabled && this.checked !== checked) {
this.checked = checked
}
},
handleClick () {
if (!this.disabled) {
const newCheckedStatus = !this.checked
this.checked = newCheckedStatus
this.$emit('click', newCheckedStatus, this.value)
}
},
handleMouseEnter (e) {
if (!this.disabled) {
this.$emit('mouseenter', e)
}
},
handleMouseLeave (e) {
if (!this.disabled) {
this.$emit('mouseleave', e)
}
},
leave () {
this.$el.classList.add('n-transfer-list-item--leave')
}
}
}
</script>

View File

@ -0,0 +1,15 @@
export default function (Vue) {
if (!Vue.options.components.PropsUnsafeTransition) {
const PropsUnsafeTransition = { ...(Vue.options.components.Transition) }
PropsUnsafeTransition.name = 'PropsUnsafeTransition'
PropsUnsafeTransition.props = {
name: {
validator: () => true
},
appear: {
validator: () => true
}
}
Vue.component(PropsUnsafeTransition.name, PropsUnsafeTransition)
}
}

View File

@ -0,0 +1,11 @@
function createValidator (types) {
return value => {
for (let i = 0; i < types.length; ++i) {
// eslint-disable-next-line valid-typeof
if (typeof value === types[i]) return true
}
return false
}
}
export default createValidator

View File

@ -1,6 +1,76 @@
@import './mixins/mixins.scss';
@import './themes/vars.scss';
@keyframes slide-in-from-left {
0% {
max-height: 0;
transform: translateX(-100%);
}
50% {
max-height: 34px;
transform: translateX(-100%);
}
100% {
max-height: 34px;
transform: translateX(0);
}
}
@keyframes slide-out-to-right {
0% {
max-height: 34px;
transform: translateX(0%);
}
50% {
max-height: 34px;
transform: translateX(100%);
}
100% {
max-height: 0px;
transform: translateX(100%);
}
}
@keyframes slide-in-from-right {
0% {
max-height: 0;
transform: translateX(100%);
}
50% {
max-height: 34px;
transform: translateX(100%);
}
100% {
max-height: 34px;
transform: translateX(0);
}
}
@keyframes slide-out-to-left {
0% {
max-height: 34px;
transform: translateX(0%);
}
50% {
max-height: 34px;
transform: translateX(-100%);
}
100% {
max-height: 0px;
transform: translateX(-100%);
}
}
/**
* There are some theme related hard codes in transfer.
* Emmm, when I am writing these code I can't figure out a better solution.
@ -96,6 +166,11 @@
padding: 0;
margin: 0;
position: relative;
@include m(animation-disabled) {
@include b(transfer-list-item) {
animation: none !important;
}
}
}
@include b(transfer-list-light-bar) {
@include once {
@ -109,8 +184,6 @@
}
@include b(transfer-list-item) {
@include once {
@include slide-right-transition(transfer-list-item-source);
@include slide-left-transition(transfer-list-item-target);
transition: color .3s $--n-ease-in-out-cubic-bezier;
position: relative;
cursor: pointer;
@ -125,6 +198,24 @@
white-space: nowrap;
padding-right: 4px;
}
@include m(source) {
animation-fill-mode: forwards;
@include m(enter) {
animation: .3s slide-in-from-right;
}
@include m(leave) {
animation: .3s slide-out-to-right;
}
}
@include m(target) {
animation-fill-mode: forwards;
@include m(enter) {
animation: .3s slide-in-from-left;
}
@include m(leave) {
animation: .3s slide-out-to-left;
}
}
}
color: map-get($--transfer-item-text-color, 'default');
@include e(checkbox) {