Recently saw some UI library source code, native JS implementation of a time selector dry goods, can be extended to their own projects on this basis
The results are as follows:
- Click to trigger the slide, the end of the slide will return the current index value
- Can be triggered by sliding, the end of the slide will return the current index value
First look at the effect, as shown below:
Implementation logic:
- Slide distance is dynamically calculated by slide event
- Use the CSS slide effect to dynamically assign the slide distance to the list container
The HTML and CSS code is as follows:
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0, the minimum - scale = 1.0, user - scalable = no">
<title>Picker - Swipes the selection component</title>
<style>
html.body {
margin: 0;
padding: 0;
}
.picker-demo {
width: 360px;
height: 264px;
margin: auto;
}
/* Mask layer area */
.picker {
width: 100%;
height: 100%;
position: relative;
}
.mask {
position: absolute;
top: 0;
left: 0;
z-index: 2;
width: 100%;
height: 100%;
background-image: linear-gradient(180deg.hsla(0.0%.100%.0.9), hsla(0.0%.100%.0.4)), linear-gradient(0deg.hsla(0.0%.100%.0.9), hsla(0.0%.100%.0.4));
background-repeat: no-repeat;
background-position: top, bottom;
pointer-events: none;
}
.cover-border {
position: absolute;
z-index: 3;
top: 50%;
left: 16px;
right: 16px;
transform: translateY(-50%);
pointer-events: none;
}
.cover-border::after{
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
top: -50%;
right: -50%;
bottom: -50%;
left: -50%;
/* border: 0 solid #ebedf0; * /
border: 0 solid #aaa;
-webkit-transform: scale(0.5);
transform: scale(0.5);
border-width: 1px 0;
}
/* Content area */
ul.li {
list-style: none;
margin: 0;
padding: 0;
}
.picker-column {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.column-item {
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
color: # 000;
}
</style>
</head>
<body>
<! -- External container -->
<div class="picker-demo">
<! - component - >
<div class="picker">
<! -- Scroll content -->
<div class="picker-column">
<ul ref="wrapper" class="wrapper-container">
<li class="column-item">1</li>
<li class="column-item">2</li>
<li class="column-item">3</li>
<li class="column-item">4</li>
<li class="column-item">5</li>
<li class="column-item">6</li>
<li class="column-item">7</li>
<li class="column-item">8</li>
<li class="column-item">9</li>
<li class="column-item">10</li>
</ul>
</div>
<! -- Shading layer -->
<div class="mask"></div>
<div class="cover-border" :style="frameStyle"></div>
</div>
</div>
</body>
<script src="./index.js"></script>
</html>
Copy the code
The logical code is as follows:
/ / the demo data
// const DEMO_DATA = ['2011', '2012', '2013', '2014', '2015', '2016', '2017', '2018', '2019', '2020']
class Picker {
DEFAULT_DURATION = 200;
MIN_DISTANCE = 10;
// This is the same as the previous one.
// When the finger leaves the screen, if the interval between the last move and 'MOMENTUM_LIMIT_TIME' is less than 'MOMENTUM_LIMIT_TIME' and move
// Perform inertial slide when the distance is greater than 'MOMENTUM_LIMIT_DISTANCE'
MOMENTUM_LIMIT_TIME = 30;
MOMENTUM_LIMIT_DISTANCE = 15;
supportsPassive = false;
constructor(options = {}) {
this.initValue(options)
this._initValue(options)
this.resetTouchStatus()
this.initComputed(options)
this.setEleStyle()
this.onMounted()
}
// Private variables
_initValue(options) {
this.offset = 0
this.duration = 0
this.options = this.initOptions
this.direction = options.direction || 'vertical'
this.deltaX = 0
this.deltaY = 0
this.offsetX = 0
this.offsetY = 0
this.startX = 0
this.startY = 0
this.moving = false
this.startOffset = 0
this.transitionEndTrigger = null // Scroll function
this.touchStartTime = 0 // Record the start time of the slide
this.momentumOffset = 0 // Record the start position of the slide
this.currentIndex = this.defaultIndex
}
// Initialize -- user variable
initValue(options) {
// But the number of subelements
this.visibleItemCount = Number(options.visibleItemCount || 6) | |6
// Child element height
this.itemPxHeight = Number(this.itemPxHeight) || 44
// Initialize the list of incoming data (used in the current case, can be used with the framework)
this.initOptions = options.initOptions || DEMO_DATA
// Read-only or not
this.readonly = options.readonly || false
// Initial display element (currently not used in the case, can be extended with the framework)
this.defaultIndex = Number(options.defaultIndex) || 0
}
// Get the computed properties based on the passed variable --
initComputed(options) {
// The height of the outer container
this.wrapHeight = this.itemPxHeight * this.visibleItemCount
this.maskStyle = { backgroundSize: 100% `The ${(this.wrapHeight - this.itemPxHeight) / 2}px` }
this.frameStyle = { height: `The ${this.itemPxHeight}px` }
// this.count = this.options.length
this.count = document.querySelector('.wrapper-container').children.length
this.baseOffset = (this.itemPxHeight * (this.visibleItemCount - 1)) / 2
// Calculate the height of the inner element
this.wrapperStyle = {
transform: `translate3d(0, The ${this.offset + this.baseOffset}px, 0)`.transitionDuration: `The ${this.duration}ms`.transitionProperty: this.duration ? 'all' : 'none',}}// Set the external container style and mask layer
setEleStyle() {
let mask = document.querySelector('.mask')
let coverBorder = document.querySelector('.cover-border')
let columnItem = document.querySelectorAll('.column-item')
mask.style.backgroundSize = this.maskStyle.backgroundSize
coverBorder.style.height = this.frameStyle.height
this.setUlStyle()
this.setColumnHeight(columnItem)
}
// Slide main logic -- dynamically sets the vertical offset of the container
setUlStyle() {
let wrapperContainer = document.querySelector('.wrapper-container')
wrapperContainer.style.transform = this.wrapperStyle.transform
wrapperContainer.style.transitionDuration = this.wrapperStyle.transitionDuration
wrapperContainer.style.transitionProperty = this.wrapperStyle.transitionProperty
}
setUlTransform() {
this.initComputed()
this.setUlStyle()
}
// Set the height and click event for each row element
setColumnHeight(columnItem) {
columnItem.forEach((item, index) = > {
item.style.height = `The ${this.itemPxHeight}px`
item.tabindex = index
item.onclick = () = > {
this.onClickItem(index)
this.setUlTransform()
}
})
}
// Click on a single row element
onClickItem(index) {
if (this.moving || this.readonly) {
return;
}
this.transitionEndTrigger = null;
this.duration = this.DEFAULT_DURATION;
this.setIndex(index, true);
}
// Initialization complete -- Perform event binding
onMounted() {
let el = document.querySelector('.picker-column')
this.bindTouchEvent(el)
}
bindTouchEvent(el) {
const { onTouchStart, onTouchMove, onTouchEnd, onTransitionEnd } = this
let wrapper = document.querySelector('.wrapper-container')
this.on(el, 'touchstart', onTouchStart);
this.on(el, 'touchmove', onTouchMove);
this.on(wrapper, 'transitionend', onTransitionEnd)
if (onTouchEnd) {
this.on(el, 'touchend', onTouchEnd);
this.on(el, 'touchcancel', onTouchEnd); }}on(target, event, handler, passive = false) {
target.addEventListener(
event,
handler,
this.supportsPassive ? { capture: false, passive } : false
);
}
// Animation end event
onTransitionEnd = () = > {
this.stopMomentum();
}
// Obtain and optimize data after sliding
stopMomentum() {
this.moving = false;
this.duration = 0;
if (this.transitionEndTrigger) {
this.transitionEndTrigger();
this.transitionEndTrigger = null; }}// Start the slide
onTouchStart = (event) = > {
// Control read-only
if (this.readonly) return
let wrapper = document.querySelector('.wrapper-container')
this.touchStart(event)
if (this.moving) {
const translateY = this.getElementTranslateY(wrapper);
this.offset = Math.min(0, translateY - this.baseOffset);
this.startOffset = this.offset;
} else {
this.startOffset = this.offset;
}
this.duration = 0;
this.transitionEndTrigger = null;
this.touchStartTime = Date.now();
this.momentumOffset = this.startOffset;
// Set slide
this.setUlTransform()
}
touchStart(event) {
this.resetTouchStatus();
this.startX = event.touches[0].clientX;
this.startY = event.touches[0].clientY;
}
// Reset the slide data variable
resetTouchStatus() {
this.direction = ' ';
this.deltaX = 0;
this.deltaY = 0;
this.offsetX = 0;
this.offsetY = 0;
}
// Get the element sliding distance dynamically -- key
getElementTranslateY(element) {
const style = window.getComputedStyle(element);
const transform = style.transform || style.webkitTransform;
const translateY = transform.slice(7, transform.length - 1).split(', ') [5];
return Number(translateY);
}
onTouchMove = (event) = > {
if (this.readonly) return
this.touchMove(event)
if (this.direction === 'vertical') {
this.moving = true;
this.preventDefault(event, true);
}
this.offset = this.range(this.startOffset + this.deltaY, -(this.count * this.itemPxHeight), this.itemPxHeight);
const now = Date.now()
if (now - this.touchStartTime > this.MOMENTUM_LIMIT_TIME) {
this.touchStartTime = now;
this.momentumOffset = this.offset;
}
/ / slide
this.setUlTransform()
}
onTouchEnd = (event) = > {
if (this.readonly) return
const distance = this.offset - this.momentumOffset;
const duration = Date.now() - this.touchStartTime;
const allowMomentum = duration < this.MOMENTUM_LIMIT_TIME && Math.abs(distance) > this.MOMENTUM_LIMIT_DISTANCE
if (allowMomentum) {
this.momentum(distance, duration);
return;
}
const index = this.getIndexByOffset(this.offset);
this.duration = this.DEFAULT_DURATION;
this.setIndex(index, true)
// The slide ends
this.setUlTransform()
// compatible with desktop scenario
// use setTimeout to skip the click event triggered after touchstart
setTimeout(() = > {
this.moving = false
}, 0);
}
// Slide animation function -- key
momentum(distance, duration) {
const speed = Math.abs(distance / duration);
distance = this.offset + (speed / 0.003) * (distance < 0 ? -1 : 1);
const index = this.getIndexByOffset(distance);
this.duration = +this.swipeDuration;
this.setIndex(index, true);
}
// Get the element data information currently displayed -- key
setIndex(index, emitChange) {
index = this.adjustIndex(index) || 0;
const offset = -index * this.itemPxHeight;
const trigger = () = > {
if(index ! = =this.currentIndex) {
this.currentIndex = index;
if (emitChange) {
// this.$emit('change', index);
console.log(index)
}
}
};
// trigger the change event after transitionend when moving
if (this.moving && offset ! = =this.offset) {
this.transitionEndTrigger = trigger;
} else {
trigger();
}
this.offset = offset;
}
getValue() {
return this.options[this.currentIndex];
}
adjustIndex(index) {
index = this.range(index, 0.this.count);
for (let i = index; i < this.count; i++) {
if (!this.isOptionDisabled(this.options[i])) return i;
}
for (let i = index - 1; i >= 0; i--) {
if (!this.isOptionDisabled(this.options[i])) returni; }}isOptionDisabled(option) {
return this.isObject(option) && option.disabled;
}
isObject(val) {
returnval ! = =null && typeof val === 'object';
}
// Slide offset
getIndexByOffset(offset) {
return this.range(Math.round(-offset / this.itemPxHeight), 0.this.count - 1);
}
// Block the default behavior
preventDefault(event, isStopPropagation) {
/* istanbul ignore else */
if (typeofevent.cancelable ! = ='boolean' || event.cancelable) {
event.preventDefault();
}
if (isStopPropagation) {
this.stopPropagation(event); }}stopPropagation(event) {
event.stopPropagation();
}
touchMove(event) {
const touch = event.touches[0];
this.deltaX = touch.clientX - this.startX;
this.deltaY = touch.clientY - this.startY;
this.offsetX = Math.abs(this.deltaX);
this.offsetY = Math.abs(this.deltaY);
this.direction = this.direction || this.getDirection(this.offsetX, this.offsetY);
}
// Determine the sliding direction
getDirection(x, y) {
if (x > y && x > this.MIN_DISTANCE) {
return 'horizontal';
}
if (y > x && y > this.MIN_DISTANCE) {
return 'vertical';
}
return ' ';
}
// Slide range limits -- key code
range(num, min, max) {
return Math.min(Math.max(num, min), max); }}new Picker()
Copy the code
The above code is a basic version of the sliding Picker, which can be extended with the framework as needed