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:

  1. Click to trigger the slide, the end of the slide will return the current index value
  2. 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:

  1. Slide distance is dynamically calculated by slide event
  2. 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