UNPKG

hg-citypicker

Version:

a citypicker in the mobile terminal

549 lines (540 loc) 17.4 kB
import { setChildStyle, generateUUID, $id, calculateBuffer, $removeSelf } from './utils' const property = Symbol('property') export default class CityPicker { constructor (config) { this.data = config.data // json 数据,必填 this.initValue = config.initValue || null // 规定初始显示的选项,选填 this.valueKey = config.valueKey || 'value' // 需要展示的数据的键名,选填 this.childKey = config.childKey || 'child' // 子数据的键名,选填 this.onOk = config.onOk // 确定按钮回调函数,必填 this.onCancel = config.onCancel || null // 取消按钮回调函数,选填 this.title = config.title || '' // 选择器标题,选填 this.okText = config.okText || '确定' // 确定按钮文本,选填 this.cancelText = config.cancelText || '取消' // 取消按钮文本,选填 this.a = config.a || 0.001 // 惯性滚动加速度(正数, 单位 px/(ms * ms)),选填,默认 0.001 this.style = config.style // 选择器样式, 选填 this[property] = {} // 存放自定义的属性 this.initTab() // 初始化标签 this.initUI() // 初始化UI this.initEvent() // 初始化事件 } /** * 定义初始化标签函数 */ initTab () { this.wrapId = generateUUID() + '-wrap' // 选择器外包裹元素ID this.relatedArr = [] // 存放每列地址的关联数组 this.cityIndex = [] // 存放每列地址的索引 this.liNum = [] // 每个ul有多少个可选li this.ulCount = 0 // 当前展示的列数 this.renderCount = 0 // 将要渲染的列数 this.liHeight = this.style && this.style.liHeight ? this.style.liHeight : 40 // 每个li的高度 this.btnHeight = this.style && this.style.btnHeight ? this.style.btnHeight : 44 // 按钮的高度 this.cityUl = [] // 每个ul元素 this.curDis = [] // 每个ul当前偏离的距离 this.curPos = [] // 记录 touchstart 时每个ul的竖向距离 this.startY = 0 // touchstart的位置 this.startTime = 0 // touchstart的时间 this.endTime = 0 // touchend的时间 this.moveY = 0 // touchmove的位置 this.moveTime = 0 // touchmove的时间 this.moveNumber = 1 // touchmove规定时间间隔下的次数 this.moveSpeed = [] // touchmove规定时间间隔下的平均速度 this.abled = true // 标识滚动是否进行中 this.containerId = this.wrapId + '-container' // 选择器容器ID this.boxId = this.wrapId + '-box' // 选择器按钮区域ID this.contentId = this.wrapId + '-content' // 选择器选择区域ID this.cancelId = this.wrapId + '-cancel' // 选择器取消按钮ID this.okId = this.wrapId + '-ok' // 选择器确定按钮ID this.titleId = this.wrapId + '-title' // 选择器确定按钮ID } /** * 定义初始化 UI 函数 */ initUI () { // 创建选择器的外包裹元素 this.createContainer() // 初始化最高层的参数,最高层的关联数组在未来的操作中都无需更新 this.relatedArr[0] = this.data this.liNum[0] = this.relatedArr[0].length if (this.initValue) { this.setInitailOption(this.initValue, true) } else { this.cityIndex[0] = 0 this.curDis[0] = 0 // 得到各列的关联数组 this.getRelatedArr(this.relatedArr[0][0], 0) // 初始化子数据参数,子数据的关联数组会随着选中父数据的改变而变化 this.updateChildData(0) // 初始化选择器内容 this.renderContent() } } /** * 定义初始化事件函数 */ initEvent () { this.container = $id(this.containerId) // 点击保存按钮隐藏选择器并输出结果 $id(this.okId).addEventListener('click', () => { this.onOk(this.getResult()) this.hide() }) // 点击取消隐藏选择器 $id(this.cancelId).addEventListener('click', () => { this.onCancel && this.onCancel() this.hide() }) // 点击背景隐藏选择器 this.wrap.addEventListener('click', (e) => { if ( e.target.id === this.wrapId && this.wrap.classList.contains('hg-picker-bg-show') ) { this.onCancel && this.onCancel() this.hide() } }) } /** * 计算并返回当前项所在的位置 * Explain : @arr 需要初始显示的项 * @isInit 是否是初始化状态 */ setInitailOption (arr, isInit) { let idxArr = [] for (let i = 0; i < arr.length; i++) { if (i === 0) { let idx = this.getValue(this.data).indexOf(arr[i]) if (idx > -1) idxArr.unshift(idx) else throw Error('The matching initValue cannot be found') } else { this.getRelatedArr(this.relatedArr[i - 1][idxArr[0]], i - 1) let idx = this.getValue(this.relatedArr[i]).indexOf(arr[i]) if (idx > -1) idxArr.unshift(idx) else throw Error('The matching initValue cannot be found') } } let idxMark = idxArr.reverse() this.ulCount = idxMark.length this.cityIndex = idxMark for (let i = 0; i < idxMark.length; i++) { this.curDis[i] = -1 * this.liHeight * idxMark[i] if (i >= 1) this.liNum[i] = this.relatedArr[i].length } if (isInit) { // 初始化选择器内容 this.renderContent() for (let i = 0; i < this.ulCount; i++) this.roll(i) } else { for (let i = 0; i < this.ulCount; i++) { this.updateView(i) this.roll(i) } } } /** * 创建选择器外包裹元素 */ createContainer () { let div = document.createElement('div') div.id = this.wrapId document.body.appendChild(div) this.wrap = $id(this.wrapId) this.wrap.classList.add('hg-picker-bg') } /** * 获取当前列后的关联数组 * Explain : @obj 当前选中数据的子数据 @i 当前操作列索引 */ getRelatedArr (obj, i) { if (typeof obj === 'object') { if (this.childKey in obj && obj[this.childKey].length > 0) { this.relatedArr[i + 1] = obj[this.childKey] this.renderCount++ this.getRelatedArr(obj[this.childKey][0], ++i) } } } /** * 更新 ulCount 和子数据的参数 * Explain : @i 当前操作列索引 当前操作列的关联数组不需要更新,只需更新其子数据中的关联数组 ulCount, liNum, cityIndex, curDis */ updateChildData (i) { this.ulCount = i + 1 + this.renderCount for (let j = i + 1; j < this.ulCount; j++) { this.liNum[j] = this.relatedArr[j].length this.cityIndex[j] = 0 this.curDis[j] = 0 } } /** * 获取每列关联数据中需要被展示的数据 * Return : Array * Explain : @arr 需要被取值的对象数组 */ getValue (arr) { let tempArr = [] for (let i = 0; i < arr.length; i++) { if (typeof arr[i][this.valueKey] === 'object') { tempArr.push(arr[i][this.valueKey][this.valueKey]) } else tempArr.push(arr[i][this.valueKey]) } return tempArr } /** * 渲染地区选择器的内容 */ renderContent () { let btnHTML = '<div class="hg-picker-btn-box" id="' + this.boxId + '">' + '<div class="hg-picker-btn" id="' + this.cancelId + '">' + this.cancelText + '</div>' + '<div class="hg-picker-btn" id="' + this.okId + '">' + this.okText + '</div>' + '<span id="' + this.titleId + '" >' + this.title + '</span> ' + '</div>' let contentHtml = '<div class="hg-picker-content" id="' + this.contentId + '">' + '<div class="hg-picker-up-shadow"></div>' + '<div class="hg-picker-down-shadow"></div>' + '<div class="hg-picker-line"></div>' + '</div>' let html = '' // 设置按钮位置 if (this.style && this.style.btnLocation === 'bottom') { html = '<div class="hg-picker-container" id="' + this.containerId + '">' + contentHtml + btnHTML + '</div>' } else { html = '<div class="hg-picker-container" id="' + this.containerId + '">' + btnHTML + contentHtml + '</div>' } this.wrap.innerHTML = html for (let i = 0; i < this.ulCount; i++) { this.renderUl(i) this.bindRoll(i) } this.setStyle() this.setUlWidth() } /** * 设置选择器样式 */ setStyle () { if (!this.style) return let obj = this.style let container = $id(this.containerId) let content = $id(this.contentId) let box = $id(this.boxId) let sureBtn = $id(this.okId) let cancelBtn = $id(this.cancelId) let len = content.children.length // 设置高度 if (obj.liHeight !== 40) { for (let i = 0; i < this.ulCount; i++) { setChildStyle(content.children[i], 'height', this.liHeight + 'px') } content.children[len - 3].style.height = this.liHeight * 2 + 'px' content.children[len - 2].style.height = this.liHeight * 2 + 'px' content.children[len - 1].style.height = this.liHeight + 'px' content.children[len - 1].style.top = this.liHeight * 2 + 'px' content.style.height = this.liHeight * 5 + 'px' content.style.lineHeight = this.liHeight + 'px' } if (obj.btnHeight !== 44) { box.style.height = this.btnHeight + 'px' box.style.lineHeight = this.btnHeight + 'px' } if (obj.btnOffset) { sureBtn.style.marginRight = obj.btnOffset cancelBtn.style.marginLeft = obj.btnOffset } if (obj.liHeight !== 40 || obj.btnHeight !== 44) { container.style.height = this.liHeight * 5 + this.btnHeight + 'px' } // 设置配色 if (obj.titleColor) box.style.color = obj.titleColor if (obj.sureColor) sureBtn.style.color = obj.sureColor if (obj.cancelColor) cancelBtn.style.color = obj.cancelColor if (obj.btnBgColor) box.style.backgroundColor = obj.btnBgColor if (obj.contentColor) content.style.color = obj.contentColor if (obj.contentBgColor) { content.style.backgroundColor = obj.contentBgColor } if (obj.upShadowColor) { content.children[len - 3].style.backgroundImage = obj.upShadowColor } if (obj.downShadowColor) { content.children[len - 2].style.backgroundImage = obj.downShadowColor } if (obj.lineColor) { content.children[len - 1].style.borderColor = obj.lineColor } } /** * 渲染 ul 元素 * Explain : @i 需要处理的列的索引 */ renderUl (i) { let parentNode = $id(this.contentId) let newUl = document.createElement('ul') newUl.setAttribute('id', this.wrapId + '-ul-' + i) parentNode.insertBefore( newUl, parentNode.children[parentNode.children.length - 3] ) this.cityUl[i] = $id(this.wrapId + '-ul-' + i) this.renderLi(i) } /** * 渲染 li 元素 * Explain : @i 需要处理的列的索引 */ renderLi (i) { this.cityUl[i].innerHTML = '' let lis = '<li></li><li></li>' this.getValue(this.relatedArr[i]).forEach(function (val, index) { lis += '<li>' + val + '</li>' }) lis += '<li></li><li></li>' this.cityUl[i].innerHTML = lis if (this.liHeight !== 40) { setChildStyle(this.cityUl[i], 'height', this.liHeight + 'px') } } /** * 设置 ul 元素宽度 */ setUlWidth () { for (let i = 0; i < this.ulCount; i++) { this.cityUl[i].style.width = (100 / this.ulCount).toFixed(2) + '%' } } /** * 绑定滑动事件 * Explain : @i 需要处理的列的索引 */ bindRoll (i) { this.cityUl[i].addEventListener( 'touchstart', () => { this.touch(i) }, false ) this.cityUl[i].addEventListener( 'touchmove', () => { this.touch(i) }, false ) this.cityUl[i].addEventListener( 'touchend', () => { this.touch(i) }, true ) } /** * 控制列表的滚动 * Explain : @i 需要处理的列的索引 * @time 滚动持续时间 */ roll (i, time) { if (this.curDis[i] || this.curDis[i] === 0) { this.cityUl[i].style.transform = 'translate3d(0, ' + this.curDis[i] + 'px, 0)' this.cityUl[i].style.webkitTransform = 'translate3d(0, ' + this.curDis[i] + 'px, 0)' if (time) { this.cityUl[i].style.transition = 'transform ' + time + 's ease-out' this.cityUl[i].style.webkitTransition = '-webkit-transform ' + time + 's ease-out' } } } /** * 地区选择器触摸事件 * Explain : @i 需要处理的列的索引 */ touch (i) { let event = window.event event.preventDefault() switch (event.type) { case 'touchstart': this.startTime = new Date() // 列表滚动中禁止二次操作 if (this.startTime - this.endTime < 200) { this.abled = false return } else this.abled = true this.startY = event.touches[0].clientY this.curPos[i] = this.curDis[i] // 记录当前位置 this.moveNumber = 1 this.moveSpeed = [] break case 'touchmove': if (!this.abled) return event.preventDefault() this.moveY = event.touches[0].clientY let offset = this.startY - this.moveY // 向上为正数,向下为负数 this.curDis[i] = this.curPos[i] - offset if (this.curDis[i] >= 1.5 * this.liHeight) { this.curDis[i] = 1.5 * this.liHeight } if (this.curDis[i] <= -1 * (this.liNum[i] - 1 + 1.5) * this.liHeight) { this.curDis[i] = -1 * (this.liNum[i] - 1 + 1.5) * this.liHeight } this.roll(i) // 每运动 130 毫秒,记录一次速度 if (this.moveTime - this.startTime >= 130 * this.moveNumber) { this.moveNumber++ this.moveSpeed.push(offset / (this.moveTime - this.startTime)) } break case 'touchend': if (!this.abled) return this.endTime = Date.now() let speed = null if (this.moveNumber === 1) { speed = (this.startY - event.changedTouches[0].clientY) / (this.endTime - this.startTime) } else { speed = this.moveSpeed[this.moveSpeed.length - 1] } this.curDis[i] = this.curDis[i] - calculateBuffer(speed, this.a) this.fixate(i) break } } /** * 固定 ul 最终的位置、更新视图 * Explain : @i 需要处理的列的索引 */ fixate (i) { this.renderCount = 0 this.getPosition(i) this.getRelatedArr(this.relatedArr[i][this.cityIndex[i]], i) this.updateChildData(i) this.updateView(i) for (let j = i; j < this.ulCount; j++) this.roll(j, 0.2) } /** * 获取定位数据 * Explain : @i 需要处理的列的索引 */ getPosition (i) { if (this.curDis[i] <= -1 * (this.liNum[i] - 1) * this.liHeight) { this.cityIndex[i] = this.liNum[i] - 1 } else if (this.curDis[i] >= 0) this.cityIndex[i] = 0 else this.cityIndex[i] = -1 * Math.round(this.curDis[i] / this.liHeight) this.curDis[i] = -1 * this.liHeight * this.cityIndex[i] } /** * 更新内容区视图 * Explain : @i 需要处理的列的索引 */ updateView (i) { let curUlCount = $id(this.contentId).children.length - 3 if (this.ulCount === curUlCount) { // 列数不变的情况 for (let j = i + 1; j < this.ulCount; j++) { this.renderLi(j) } } else if (this.ulCount > curUlCount) { // 列数增加的情况 for (let j = i + 1; j < curUlCount; j++) { this.renderLi(j) } for (let j = curUlCount; j < this.ulCount; j++) { this.renderUl(j) this.bindRoll(j) } this.setUlWidth() } else { // 列数减少的情况 for (let j = i + 1; j < this.ulCount; j++) { this.renderLi(j) } for (let j = this.ulCount; j < curUlCount; j++) { $removeSelf(this.cityUl[j]) } this.setUlWidth() } } /** * 获取结果的数组 */ getResult () { let arr = [] for (let i = 0; i < this.ulCount; i++) { arr.push(this.relatedArr[i][this.cityIndex[i]]) } return arr } /** * 显示选择器 */ show () { this.wrap.classList.add('hg-picker-bg-show') this.container.classList.add('hg-picker-container-up') } /** * 隐藏选择器 */ hide () { this.wrap.classList.remove('hg-picker-bg-show') this.container.classList.remove('hg-picker-container-up') } /** * 设置选择器属性 */ set (obj) { for (let [key, value] of Object.entries(obj)) { if (/^(title|cancelText|okText|valueKey|childKey|a|onOk|onCancel|initValue)$/.test(key)) { this[key] = value if (key === 'title') $id(this.titleId).innerHTML = value else if (key === 'okText') $id(this.okId).innerHTML = value else if (key === 'cancelText') $id(this.cancelId).innerHTML = value else if (key === 'initValue') this.setInitailOption(value) } else { this[property][key] = value } } return this } /** * 获取选择器属性 */ get (key) { if (/^(title|cancelText|okText|valueKey|childKey|a|onOk|onCancel|initValue)$/.test(key)) { return this[key] } else { return this[property][key] } } /** * 销毁组件 */ destroy () { $id(this.wrapId).remove() } }