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"> <n-button @click="regenValues">
Regen Values Regen Values
</n-button> </n-button>
<pre class="n-doc-section__inspect">{{ JSON.stringify(value) }}</pre> <!-- <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">{{ $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 ```js
let prefix = null let prefix = null
function genOptions () { function genOptions () {
prefix = Math.random().toString(36).slice(2, 5) 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, label: prefix + 'Option' + i,
value: prefix + i, value: prefix + i,
disabled: i % 3 === 0 disabled: i % 5 === 0
})) }))
} }
function genValues () { 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 { export default {

View File

@ -58,7 +58,7 @@ export default {
], ],
model: { model: {
prop: 'checked', prop: 'checked',
event: 'input' event: 'change'
}, },
props: { props: {
value: { value: {
@ -103,8 +103,7 @@ export default {
if (this.NCheckboxGroup) { if (this.NCheckboxGroup) {
this.NCheckboxGroup.toggleCheckbox(!this.synthesizedChecked, this.value) this.NCheckboxGroup.toggleCheckbox(!this.synthesizedChecked, this.value)
} else { } else {
this.$emit('input', !this.synthesizedChecked) this.$emit('change', !this.synthesizedChecked, this.synthesizedChecked)
this.$emit('change', !this.synthesizedChecked, this.value)
} }
}, },
handleClick (e) { 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 */ /* istanbul ignore file */
import Transfer from './src/Transfer.vue' import Transfer from './src/Transfer.vue'
import installPropsUnsafeTransition from '../../utils/installPropsUnsafeTransition'
Transfer.install = function (Vue) { Transfer.install = function (Vue) {
installPropsUnsafeTransition(Vue)
Vue.component(Transfer.name, Transfer) Vue.component(Transfer.name, Transfer)
} }

View File

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

View File

@ -21,15 +21,25 @@
</template> </template>
<script> <script>
import createValidator from '../../../utils/validateProp'
export default { export default {
props: { props: {
to: { to: {
type: Boolean, validator: createValidator(['boolean']),
default: false default: false
}
}, },
disabled: { inject: {
type: Boolean, NTransfer: {
default: false 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: { methods: {

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 './mixins/mixins.scss';
@import './themes/vars.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. * There are some theme related hard codes in transfer.
* Emmm, when I am writing these code I can't figure out a better solution. * Emmm, when I am writing these code I can't figure out a better solution.
@ -96,6 +166,11 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
position: relative; position: relative;
@include m(animation-disabled) {
@include b(transfer-list-item) {
animation: none !important;
}
}
} }
@include b(transfer-list-light-bar) { @include b(transfer-list-light-bar) {
@include once { @include once {
@ -109,8 +184,6 @@
} }
@include b(transfer-list-item) { @include b(transfer-list-item) {
@include once { @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; transition: color .3s $--n-ease-in-out-cubic-bezier;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
@ -125,6 +198,24 @@
white-space: nowrap; white-space: nowrap;
padding-right: 4px; 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'); color: map-get($--transfer-item-text-color, 'default');
@include e(checkbox) { @include e(checkbox) {