mand-mobile/components/picker/picker-column.vue

479 lines
14 KiB
Vue

<template>
<div class="md-picker-column" :style="{ height: `${style.indicatorHeight + 2 * style.maskerHeight}px` }">
<div class="md-picker-column-container">
<div class="md-picker-column-masker top" :style="{ height: `${style.maskerHeight}px` }"></div>
<div class="md-picker-column-masker bottom" :style="{ height: `${style.maskerHeight}px` }"></div>
<div class="md-picker-column-list">
<template v-for="(colunm, i) in columnValues">
<div
class="md-picker-column-item"
:key="i"
>
<ul class="column-list" :style="{ 'padding-top': `${style.maskerHeight}px` }">
<template v-for="(item, j) in colunm">
<li
class="column-item"
:class="{'disabled': $_isColumnIndexInvalid(i, j)}"
:style="{
'height': `${style.indicatorHeight}px`,
'line-height': `${style.indicatorHeight}px`
}"
:key="j"
v-text="item.text || item.label"
>
</li>
</template>
</ul>
</div>
</template>
<template v-for="n in (cols - columnValues.length)">
<div class="md-picker-column-item" :key="n + columnValues.length - 1">
<ul class="column-list" :style="{ 'padding-top': `${style.maskerHeight}px` }"></ul>
</div>
</template>
</div>
<div class="md-picker-column-hooks">
<template v-for="n in cols">
<div
class="md-picker-column-hook"
:key="n - 1"
@touchstart="$_onColumnTouchStart($event, n - 1)"
@mousedown="$_onColumnTouchStart($event, n - 1, true)"
@touchmove="$_onColumnTouchMove($event, n - 1)"
@mousemove="$_onColumnTouchMove($event, n - 1, true)"
@touchend="$_onColumnTouchEnd($event, n - 1)"
@mouseup="$_onColumnTouchEnd($event, n - 1, true)"
></div>
</template>
</div>
</div>
</div>
</template>
<script>
import Scroller from '../_util/scroller'
import {render} from '../_util/render'
import {noop, getDpr, traverse, inArray, compareObjects, warn} from '../_util'
const dpr = getDpr()
const API_LIST = [
'getColumnValue',
'getColumnValues',
'getColumnIndex',
'getColumnIndexs',
'getColumnIndexByDefault',
'setColumnValues',
'refresh',
'inheritPickerApi',
]
export default {
name: 'md-picker-column',
props: {
data: {
type: Array,
default() {
return []
},
},
cols: {
type: Number,
default: 1,
},
defaultValue: {
type: Array,
default() {
return []
},
},
defaultIndex: {
type: Array,
default() {
return []
},
},
invalidIndex: {
type: Array,
default() {
return []
},
},
},
data() {
return {
style: {
maskerHeight: 81 * dpr,
indicatorHeight: 36 * dpr,
},
columnValues: [],
scrollers: [],
scrollDirect: 1,
scrollPosition: 0,
activedIndexs: [],
isInitialed: false,
isScrollInitialed: false,
isScrolling: false,
isMouseDown: false,
}
},
watch: {
data: {
handler(val, oldVal) {
/* istanbul ignore if */
if (!compareObjects(val, oldVal)) {
this.columnValues = val
}
},
deep: true,
},
},
created() {
this.columnValues = this.data
},
methods: {
// MARK: private methods
// initial scroller for each column
$_initColumnsScroller(startIndex = 0) {
let hooks = this.$el.querySelectorAll('.md-picker-column-hook')
/* istanbul ignore if */
if (!hooks) {
return
}
hooks = Array.isArray(hooks) ? hooks : Array.prototype.slice.call(hooks)
for (let i = startIndex, len = hooks.length; i < len; i++) {
const container = hooks[i]
container && this.$_initSingleColumnScroller(container, i)
}
// initial index only refresh all columns
if (!startIndex) {
this.$_initColumnIndex()
if (!this.isInitialed) {
this.isInitialed = true
setTimeout(() => {
this.$emit('initialed')
}, 0)
}
}
this.isScrollInitialed = true
},
// initial scroller for column by index
$_initSingleColumnScroller(container, index) {
const columns = this.$el.querySelectorAll('.column-list')
const content = columns[index]
/* istanbul ignore if */
if (index === undefined || !columns || !container || !content) {
return
}
const rect = container.getBoundingClientRect()
const scroller = new Scroller(
(left, top) => {
render(content, left, top)
},
{
scrollingX: false,
snapping: true,
scrollingComplete: () => {
this.$_onColumnScrollEnd(index)
},
},
)
// set scroller size
scroller.setPosition(rect.left + container.clientLeft, rect.top + container.clientTop)
scroller.setDimensions(
container.clientWidth,
container.clientHeight,
content.offsetWidth,
content.offsetHeight + this.style.maskerHeight,
)
scroller.setSnapSize(0, this.style.indicatorHeight)
// save scroller instance
this.$set(this.scrollers, index, scroller)
},
// each column scroll to active item by defaultIndex
$_initColumnIndex() {
const data = this.columnValues
const scrollers = this.scrollers
const defaultValue = this.defaultValue
const defaultIndex = this.defaultIndex
this.$_getColumnIndexByDefault(data, defaultIndex, defaultValue, (columnIndex, itemIndex) => {
const scroller = scrollers[columnIndex]
const offsetTop = this.$_getColumnOffsetByIndex(itemIndex)
/* istanbul ignore if */
if (!scroller) {
warn(`initialColumnIndex: scroller of column ${columnIndex} is undefined`)
return 1
}
scroller.scrollTo(0, offsetTop)
// save active index of each column
this.$set(this.activedIndexs, columnIndex, itemIndex)
})
},
$_getColumnIndexByDefault(data, defaultIndex = [], defaultValue = [], fn = noop) {
/* istanbul ignore if */
if (!data) {
return
}
traverse(data, (item, level, indexs) => {
const columnIndex = indexs[0]
const itemIndex = indexs[1]
const itemDefaultIndex = defaultIndex[columnIndex]
const itemDefaultValue = defaultValue[columnIndex]
if (
(itemDefaultIndex !== undefined && itemIndex === itemDefaultIndex) ||
(itemDefaultValue !== undefined &&
(item.text === itemDefaultValue || item.label === itemDefaultValue || item.value === itemDefaultValue))
) {
fn(columnIndex, itemIndex)
return 2
}
})
},
$_getColumnIndexByOffset(top) {
return Math.round(top / this.style.indicatorHeight)
},
$_getColumnOffsetByIndex(index) {
return index * this.style.indicatorHeight
},
$_isColumnIndexInvalid(columnIndex, itemIndex) {
const invalidIndex = this.invalidIndex[columnIndex]
return inArray(invalidIndex, itemIndex)
},
$_scrollToValidIndex(columnIndex, itemIndex) {
const scroller = this.scrollers[columnIndex]
const dirction = this.scrollDirect
let count = itemIndex
while (this.$_isColumnIndexInvalid(columnIndex, count)) {
count += dirction
}
let offsetTop = this.$_getColumnOffsetByIndex(count)
scroller.scrollTo(0, this.$_scrollInZoon(scroller, offsetTop), true)
},
$_scrollInZoon(scroller, top) {
const MaxTop = scroller.getScrollMax().top
if (top < 0) {
return 0
} else if (top > MaxTop) {
return MaxTop
} else {
return top
}
},
// MARK: events handler
$_onColumnTouchStart(event, index, isMouse) {
event.preventDefault()
const scroller = this.scrollers[index]
const touches = isMouse ? [{pageX: event.pageX, pageY: event.pageY}] : event.touches
/* istanbul ignore if */
if (!scroller) {
warn(`touchstart: scroller of column ${index} is undefined`)
return
}
this.scrollPosition = isMouse ? event.pageY : event.touches[0].pageY
scroller.doTouchStart(touches, event.timeStamp)
isMouse && (this.isMouseDown = true)
},
$_onColumnTouchMove(event, index, isMouse) {
const scroller = this.scrollers[index]
const touches = isMouse ? [{pageX: event.pageX, pageY: event.pageY}] : event.touches
/* istanbul ignore if */
if (!scroller || (isMouse && !this.isMouseDown)) {
return
}
const diff = this.scrollPosition - (isMouse ? event.pageY : event.touches[0].pageY)
this.scrollDirect = diff ? diff / Math.abs(diff) : 1
scroller.doTouchMove(touches, event.timeStamp)
isMouse && (this.isMouseDown = true)
},
$_onColumnTouchEnd(event, index, isMouse) {
const scroller = this.scrollers[index]
/* istanbul ignore if */
if (!scroller || (isMouse && !this.isMouseDown)) {
return
}
scroller.doTouchEnd(event.timeStamp)
isMouse && (this.isMouseDown = false)
},
$_onColumnScrollEnd(index) {
const scroller = this.scrollers[index]
const top = scroller.getValues().top
const scrollTop = this.$_scrollInZoon(scroller, top)
const activeItemIndex = this.$_getColumnIndexByOffset(scrollTop)
const isInvalid = this.$_isColumnIndexInvalid(index, activeItemIndex)
if (isInvalid || activeItemIndex === this.activedIndexs[index]) {
isInvalid && this.$_scrollToValidIndex(index, activeItemIndex)
return false
}
/* istanbul ignore next */
this.$set(this.activedIndexs, index, activeItemIndex)
/* istanbul ignore next */
this.$emit('change', index, activeItemIndex, this.getColumnValue(index))
},
// MARK: public methods
inheritPickerApi(instance, blacklist = []) {
traverse(API_LIST, api => {
/* istanbul ignore if */
if (!instance) {
return 2
} else if (~blacklist.indexOf(api)) {
return 1
}
const fn = this[api]
/* istanbul ignore else */
if (fn) {
instance[api] = fn
} else {
warn(`inheritPickerApi: Api method [${api}] is undefined`)
}
})
},
getColumnValue(index = 0) {
const activeValues = this.getColumnValues()
return activeValues[index]
},
getColumnValues() {
const data = this.columnValues
const activeIndexs = this.activedIndexs
let activeValues = []
data.forEach((item, index) => {
activeValues[index] = item[activeIndexs[index]]
})
return activeValues
},
getColumnIndex(index = 0) {
return this.activedIndexs[index]
},
getColumnIndexs() {
return this.activedIndexs
},
getColumnIndexByDefault(data, defaultIndex = [], defaultValue = [], fn = noop) {
/* istanbul ignore next */
this.$_getColumnIndexByDefault(data, defaultIndex, defaultValue, fn)
},
setColumnValues(index, values, callback = noop) {
/* istanbul ignore if */
if (index === undefined || values === undefined) {
return
}
this.$set(this.activedIndexs, index, 0) // reset active index
this.$set(this.columnValues, index, values)
this.$nextTick(() => {
this.$_initSingleColumnScroller(index)
callback()
})
},
refresh(callback, startIndex = 0) {
// this.activedIndexs = []
this.$nextTick(() => {
this.$_initColumnsScroller(startIndex)
callback && callback()
})
},
},
}
</script>
<style lang="stylus">
.md-picker-column
position relative
width 100%
padding-bottom constant(safe-area-inset-bottom)
background color-bg-base
transform translate3d(0, 0, 0)
.md-picker-column-container
height 100%
.md-picker-column-masker
position absolute !important
z-index 2
left 0
right 0
transform translate3d(0, 0, 0)
&.top
top 0
background -webkit-gradient(linear,left bottom,left top,from(hsla(0, 0%,100%,.2)),to(hsla(0,0%,100%,1)))
hairline(bottom, color-border-base)
// border-bottom solid 1px color-border-base
&.bottom
bottom 0
bottom constant(safe-area-inset-bottom)
background -webkit-gradient(linear,left top,left bottom,from(hsla(0, 0%,100%,.2)),to(hsla(0,0%,100%,1)))
hairline(top, color-border-base)
// border-top solid 1px color-border-base
.md-picker-column-hooks
display flex
position absolute
z-index 3
absolute-pos()
.md-picker-column-hook
display flex
flex 1
height 100%
.md-picker-column-list
display flex
height 100%
.md-picker-column-item
position relative
display flex
flex 1
clearfix()
overflow hidden
ul.column-list
position absolute
top 0
left 0
width 100%
transform-origin left top
box-sizing border-box
transform translate3d(0, 0, 0)
li.column-item
float left
width 100%
padding 0 h-gap-md
box-sizing border-box
color picker-color
font-size picker-font-size
text-align center
word-ellipsis()
&.disabled
opacity picker-disabled-opacity
</style>